Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1f59138
Move circle definitions into the Circle table
toddmitchell Aug 26, 2026
4c2419c
Move app registrations into the AppRegistrations table
toddmitchell Aug 26, 2026
10a88f9
Make the promoted circle columns settable, and queryable by GrantOn
toddmitchell Aug 26, 2026
74a3b61
Merge branch 'main' into step-1-circle-and-app-registration-tables
toddmitchell Aug 26, 2026
8a53e44
Build clean under --warnaserror
toddmitchell Aug 26, 2026
e828350
Drop the global UNIQUE on Circle.circleId
toddmitchell Aug 26, 2026
8d51558
Create AppRegistrations on databases that never got it
toddmitchell Aug 27, 2026
c5c83a8
Merge branch 'main' into step-1-circle-and-app-registration-tables
toddmitchell Aug 27, 2026
d797a8f
Set the upgrade run state the way main now does
toddmitchell Aug 27, 2026
e36e9d9
Read the legacy app blob with the shape it was written in
toddmitchell Aug 27, 2026
6929b4d
Create the shared drive once instead of racing 20 threads to make it
toddmitchell Aug 27, 2026
fc0ed0a
Wire the drive addressing columns through
toddmitchell Aug 27, 2026
c0aefc4
Add the addressing go-live checklist
toddmitchell Aug 27, 2026
5848041
Carry the drive addressing fields on the app-route drive shape
toddmitchell Aug 27, 2026
6da8ef4
Let an app ask for its own slug at registration
toddmitchell Aug 27, 2026
66bc5c4
Update the checklist for the app slug work
toddmitchell Aug 27, 2026
6de5d76
Give every drive an owning app and a slug
toddmitchell Aug 28, 2026
ddf8acc
Register the built-in apps and their circles at identity setup
toddmitchell Aug 28, 2026
9db513f
Declare the default system as one tree
toddmitchell Aug 28, 2026
791664b
Provision an identity from the app tree
toddmitchell Aug 29, 2026
3bbfee7
Bring existing identities onto the app tree
toddmitchell Aug 29, 2026
04cb417
Move the drive constants into the tree
toddmitchell Aug 29, 2026
43536f6
Make the upgrade enforce the app tree, not just fill it in
toddmitchell Aug 29, 2026
775dcfb
Merge branch 'main' into step-1-circle-and-app-registration-tables
toddmitchell Aug 31, 2026
28813da
Give user-created channel drives an owner and an address
toddmitchell Aug 31, 2026
f918851
Let the tree name every app's slug, not a second list
toddmitchell Aug 31, 2026
e112b4a
Give Emergency Location Access one definition and one creator
toddmitchell Aug 31, 2026
973ce6e
Add the Webdrop app and give every remaining drive a slug
toddmitchell Aug 31, 2026
6f78038
Webdrop is known, not shipped with every identity
toddmitchell Aug 31, 2026
185a7f8
Answer from the blob until an identity has moved to the tables
toddmitchell Aug 31, 2026
fbf38bb
Keep the tests in step with what the tree provisions
toddmitchell Sep 1, 2026
17c62e6
Let a client ask what the upgrade is doing while it runs
toddmitchell Sep 1, 2026
763c6d6
Give Community the app id its client actually registers with
toddmitchell Sep 1, 2026
03368a4
Retire BuiltInCircleConstants
toddmitchell Sep 1, 2026
631bf63
Give the Vault drive the guids chat-kmp actually uses
toddmitchell Sep 1, 2026
f8aef05
Let an app name its own slugs, and refuse a duplicate drive slug
toddmitchell Sep 2, 2026
7494ffa
Address drives by slug, locally and over peer
toddmitchell Sep 2, 2026
64039b7
Test the slug-addressed routes, and fix what that turned up
toddmitchell Sep 2, 2026
925dd93
Stop capturing the peer query service twice
toddmitchell Sep 3, 2026
0c4815d
Put Mail back on the tree, and the three that displaced it back where…
toddmitchell Sep 3, 2026
33f95e8
Merge branch 'main' into step-1-circle-and-app-registration-tables
toddmitchell Sep 3, 2026
a82c813
Revert "Put Mail back on the tree, and the three that displaced it ba…
toddmitchell Sep 3, 2026
90a89cf
Merge remote-tracking branch 'origin/step-1-circle-and-app-registrati…
toddmitchell Sep 3, 2026
434e0e0
Make the tests say what the tree now says: no Mail app, Moments and V…
toddmitchell Sep 3, 2026
ce6b7cc
Let the tenant lock go before the post-commit notification runs
toddmitchell Sep 4, 2026
380689b
Revert "Let the tenant lock go before the post-commit notification runs"
toddmitchell Sep 4, 2026
dd0e063
Run post-transaction actions after the transaction is torn down
toddmitchell Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions docs/addressing-golive-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Go-live checklist: Circle, AppRegistrations and Drive addressing

Derived working checklist for the table moves (#1688), chat circle ownership (#1691), and the drive
addressing columns. `docs/drive-addressing.md` and `docs/connection-defaults.md` remain the specs;
where this file and those disagree, those win.

Status as of 2026-08-27.

## Done

| # | Item | Where |
|---|---|---|
| 0.1 | Circle definitions moved out of the key-three-value blob into the `Circle` table | #1688 |
| 0.2 | App registrations moved into the `AppRegistrations` table, slugs coined | #1688 |
| 0.3 | `GetByGrantOnAsync` + cache invalidation; `CreateCircleRequest` carries the promoted fields; deposit-only guard | #1688 |
| 0.4 | v12 -> v13 migration, both moves in one step, idempotent, blob rows kept | #1688 |
| 0.5 | `Circle.circleId` global UNIQUE dropped (`v202608261644`) | #1688 |
| 0.6 | `AppRegistrations` create reachable on databases that predate the table (`v202608271000`) | #1688 |
| 0.7 | Legacy blob read with a frozen DTO -- the `[JsonIgnore]` bug that migrated zero apps | #1688 |
| 0.8 | Chat app owns Friends/Family/Work/Acquaintances; v13 -> v14 rebinds ownership only | #1691 |
| 0.9 | `AppId` / `DriveSlug` / `DriveTypeSlug` plumbed through `StorageDrive`, `DriveManager`, `CreateDriveRequest`, `OwnerClientDriveData`; `OdinSlug` format validator | #1688 |
| 0.10 | The same three on `ClientDriveData` (app and V2 routes), redacted for third parties as `Name` already is | #1688 |
| 0.11 | `AppRegistrationRequest.AppSlug`: optional, format-validated, taken verbatim or refused, never silently replaced. Omitted still derives from `Name` | #1688 |

## Blocked on decisions

| # | Item | Note |
|---|---|---|
| 1.1 | Owning app per system drive | `scratchpad/drives.csv`. Not inferable: no drive is granted to exactly one app, and Contact/Profile/Sticker are granted to all three |
| 1.2 | `DriveSlug` per system drive | Same file. Defaults in it are proposals, not from the codebase |
| 1.3 | `DriveTypeSlug` values, and whether it follows the type guid or the drive | Profile/Wallet/HomePageConfig share one type guid; Moments/Lists share another. Per-type means HomePageConfig's slug reads as `profile` |
| 1.4 | Do ownerless drives get slugs at all? | `drive-addressing.md` OQ3. The doc's invariant says `DriveSlug` is null when `AppId` is null |
| 1.5 | Owning app for Confirmed / Auto-connected / Emergency Location Access | `scratchpad/circles.csv`. Emergency Location should agree with whoever owns LocationDrive |
| 1.6 | Do Lists, Location and ShardRecovery become their own apps before or after this ships? | If before, defaulting them to chat means a second ownership migration |
| 1.7 | Is `(AppSlug, DriveSlug)` the successor to `TargetDrive` or a parallel name? | `drive-addressing.md` OQ1. Decides whether slugs are an address or a convenience |

## Derivation and backfill

| # | Item |
|---|---|
| 2.1 | `DriveSlugGenerator`, mirroring `AppSlugGenerator`: fixed slugs for known drives, derived-from-name otherwise, whole set resolved with dedupe before anything is written |
| 2.2 | Derive on drive create, so drives made after the backfill are not immediately null again |
| 2.3 | Backfill migration for existing drives: ownership, slug, type slug |
| 2.4 | Backfill `Circle.AppId` for the system and built-in circles (the wizard four are already covered by #1691) |
| 2.5 | Decide whether `GrantOn` / `Designation` get set during backfill or stay at their defaults |

## Enforcement

Everything below is currently accepted-but-unenforced. Each line is a separate decision about *when*
to tighten, because each one can reject a call that works today.

| # | Item | Today |
|---|---|---|
| 3.1 | **App slug supplied by the caller** at registration | **Done** (0.11). Optional: supplied is validated and taken verbatim, omitted derives from `Name`. Requiring it is a separate decision -- it would break every client that registers an app today |
| 3.2 | **App slug immutability** across updates | Enforced: updates carry the stored slug forward, and no update request carries a slug field |
| 3.3 | **App slug uniqueness** | Enforced by `UNIQUE(identityId, AppSlug)`, and a supplied duplicate is refused with a client error before it reaches the constraint |
| 3.3a | **App slug is derived in two places** | `AppRegistrationService.AssignSlugAsync` and the v12 -> v13 migration, both through `AppSlugGenerator`, so a migrated app and a freshly registered one land on the same value. Decided 2026-08-27 to keep it that way rather than make the column nullable |
| 3.3b | **Derived slugs truncate at 12 characters** | `Acme Receipts` becomes `acme-receipt`. Fine for a derived fallback; worth saying out loud before any client relies on round-tripping a name |
| 3.4 | **Drive slug required when `AppId` is set** | Not enforced. An app-owned drive can be created with no slug |
| 3.5 | **Drive slug forbidden when `AppId` is null** | Not enforced. The doc's invariant; without it, ownerless drives can carry slugs the unique index cannot constrain (NULL `AppId` rows do not collide in either dialect) |
| 3.6 | **Drive slug uniqueness for ownerless drives** | Impossible at the constraint level while `AppId` is null; needs a code check, or 3.5 |
| 3.7 | **Drive slug immutability** | No update path writes it yet. Once slugs are addresses, renaming breaks other identities' links |
| 3.8 | **Reserved-segment denylist** | `OdinSlug.Reserved` is empty and must grow whenever a literal route segment is added at `{appSlug}` or `{driveSlug}` position |
| 3.9 | **System app slugs protected** | **Open, and now reachable.** `AppSlugGenerator` orders known apps first when *deriving*, but a caller may now supply `chat` on an identity where the chat app is not yet registered, and take it first-come. `drive-addressing.md` OQ2 (no global registry) is the same question |
| 3.10 | **`Type` becomes app-scoped** | Four call sites still read it as a global vocabulary: `FollowerService`, `FollowerPerimeterService`, `FeedDriveDistributionRouter`, `FeedNotificationMapper` |

## Routes

| # | Item |
|---|---|
| 4.1 | `/api/v2/apps/{appSlug}/drives/{driveSlug}/...` -- not built |
| 4.2 | `/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/...` -- not built. This is the payoff: neither side shares a Guid |
| 4.3 | Recipient-side slug resolution, and what happens when a slug does not resolve |
| 4.4 | Keep `type` in responses until `TargetDrive` retires end to end -- two independent steps, in that order |

## Deploy safety

| # | Item |
|---|---|
| 5.1 | **The app list reads the table exclusively.** The v12 -> v13 upgrade runs only when the owner logs in (`OwnerAuthenticationHandler` -> `EnsureScheduledAsync`), so between deploy and that login an identity has no apps and app clients break. Decide: blob fallback until migrated, a non-owner-triggered upgrade, or a two-release sequence |
| 5.2 | **Never renumber a migration that has run anywhere.** A demo box was stranded at v13 when v13 changed meaning between builds; the version number is the only record of what actually happened |
| 5.3 | Confirm no environment other than the demo box ran the intermediate two-step build |
| 5.4 | Blob cleanup job for the migrated circle and app rows -- deliberately deferred, still owed |
| 5.5 | Generator: stamp a new table's create migration at the current version, never 0, and branch `UpAsync` on table-exists. Both are hand-edited in the repo today and the next regen reverts them |
| 5.6 | Audit other tables whose migration list holds only a `V0` entry but that were added after first release -- same latent 42P01 |

## Tests owed

| # | Item |
|---|---|
| 6.1 | `TableAppRegistrationsTests` -- no CRUD test exists at all; every sibling table has one |
| 6.2 | Duplicate-slug rejection: `UNIQUE(identityId, AppSlug)` is the reason for the whole table move and nothing asserts it |
| 6.3 | **Migration from an existing database**, not an empty one. Every migration test starts empty, which is exactly the case that works -- this is the gap that let the missing-table bug reach demo |
| 6.4 | Circle table tests for the four promoted columns, `GetByGrantOnAsync`, and its cache invalidation |
| 6.5 | API-level assertion that the promoted circle fields survive a definition round-trip, and that a trimmed update body does not silently clear them |

## Client coordination

| # | Item |
|---|---|
| 7.1 | Does any client filter circles by `appId`? Stamping the relationship circles could make them vanish from a circle manager that shows only owner circles |
| 7.2 | What does the setup wizard actually post for Friends/Family/Work/Acquaintances? The server now wins the create race, so anything extra the wizard sends is silently dropped for new identities |
| 7.3 | Clients must round-trip the full circle definition on update, or `grantOn` / `designation` / `emoji` are cleared |
| 7.4 | Owner console: surface `appId` / `driveSlug` / `driveTypeSlug` where useful |
| 7.5 | Apps may now send `appSlug` at registration. Clients that want a specific address should send one and handle a refusal -- registration is first-come, and a taken slug is an error, not a silent rename |
26 changes: 26 additions & 0 deletions docs/slug-addressing-endpoint-map.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Group,Method,Guid Route (existing),Slug Route (new),Status,In Doc,Notes
Peer read,POST,/api/v2/peer/{odinId}/drives/{driveId}/query-batch,/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/query-batch,Built,Yes (files shape),1:1 twin
Peer read,GET,/api/v2/peer/{odinId}/drives/{driveId}/files/{fileId}/header,/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/{fileId}/header,Built,Yes (files shape),1:1 twin
Peer read,GET,/api/v2/peer/{odinId}/drives/{driveId}/files/{fileId}/payload/{payloadKey},/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/{fileId}/payload/{payloadKey},Built,Yes (files shape),1:1 twin
Peer read,GET,/api/v2/peer/{odinId}/drives/{driveId}/files/{fileId}/payload/{payloadKey}/{start}/{length},/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/{fileId}/payload/{payloadKey}/{start}/{length},Built,Yes (files shape),1:1 twin
Peer read,GET,/api/v2/peer/{odinId}/drives/{driveId}/files/{fileId}/payload/{payloadKey}/thumb/{width}/{height},/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/{fileId}/payload/{payloadKey}/thumb/{width}/{height},Built,Yes (files shape),1:1 twin
Peer read,GET,/api/v2/peer/{odinId}/drives/{driveId}/files/by-uid/{uid}/exists,/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/by-uid/{uid}/exists,Built,Yes (files shape),1:1 twin
Peer read,GET,/api/v2/peer/{odinId}/drives/{driveId}/files/by-gtid/{gtid}/exists,/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/by-gtid/{gtid}/exists,Built,Yes (files shape),1:1 twin
Peer read,GET,/api/v2/peer/{odinId}/drives/{driveId}/files/by-gtid/{gtid}/header,/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/by-gtid/{gtid}/header,Built,Yes (files shape),1:1 twin
Peer read,GET,/api/v2/peer/{odinId}/drives/{driveId}/files/by-gtid/{gtid}/payload/{payloadKey},/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/by-gtid/{gtid}/payload/{payloadKey},Built,Yes (files shape),1:1 twin
Peer read,GET,/api/v2/peer/{odinId}/drives/{driveId}/files/by-gtid/{gtid}/payload/{payloadKey}/{start}/{length},/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/by-gtid/{gtid}/payload/{payloadKey}/{start}/{length},Built,Yes (files shape),1:1 twin
Peer read,GET,/api/v2/peer/{odinId}/drives/{driveId}/files/by-gtid/{gtid}/payload/{payloadKey}/thumb/{width}/{height},/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/by-gtid/{gtid}/payload/{payloadKey}/thumb/{width}/{height},Built,Yes (files shape),1:1 twin
Peer temporal,POST,/api/v2/peer/{odinId}/drives/{driveId}/temporal/verify,/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/temporal/verify,Built,No,1:1 twin; temporal chain mirrors the read chain
Peer temporal,POST,/api/v2/peer/{odinId}/drives/{driveId}/temporal/query-batch,/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/temporal/query-batch,Built,No,1:1 twin
Peer temporal,GET,/api/v2/peer/{odinId}/drives/{driveId}/temporal/files/{fileId}/header,/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/temporal/files/{fileId}/header,Built,No,1:1 twin
Peer temporal,GET,/api/v2/peer/{odinId}/drives/{driveId}/temporal/files/{fileId}/payload/{payloadKey},/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/temporal/files/{fileId}/payload/{payloadKey},Built,No,1:1 twin
Peer temporal,GET,/api/v2/peer/{odinId}/drives/{driveId}/temporal/files/{fileId}/payload/{payloadKey}/{start}/{length},/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/temporal/files/{fileId}/payload/{payloadKey}/{start}/{length},Built,No,1:1 twin
Peer temporal,GET,/api/v2/peer/{odinId}/drives/{driveId}/temporal/files/{fileId}/payload/{payloadKey}/thumb/{width}/{height},/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/temporal/files/{fileId}/payload/{payloadKey}/thumb/{width}/{height},Built,No,1:1 twin
Peer write,POST,/api/v2/peer/{odinId}/drives/{driveId}/files/send,/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/send,Built,Yes (doc headline example),Route overrides body remoteTargetDrive and recipients
Peer write,POST,/api/v2/peer/{odinId}/drives/{driveId}/files/senddeleterequest,/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/files/senddeleterequest,Built,Yes (files shape),Route overrides body targetDrive and recipients
Local,GET,/api/v2/drives/metadata/channel-drives,/api/v2/apps/{appSlug}/drives?type={typeSlug},Built,Yes,Generalizes the guid route; doc says this retires it
Local,GET,(none),/api/v2/apps/{appSlug}/drives,Built,Yes,No guid equivalent existed
Local,GET,(none),/api/v2/apps/{appSlug}/drives/{driveSlug},Built,No,ONLY GENUINELY NEW CAPABILITY - single drive metadata by address
Local,ALL,/api/v2/drives/{driveId}/files/...,/api/v2/apps/{appSlug}/drives/{driveSlug}/files/...,NOT BUILT,Yes,Whole local file surface - upload update delete reactions by-uid by-gtid query
Peer read,GET,(none),/api/v2/peer/{odinId}/apps/{appSlug}/drives/{driveSlug}/public-key,NOT BUILT,Yes,Write-only deposit key; doc calls this where slugs earn their keep
Server-to-server,POST,/api/perimeter/transit/host/drive/metadata/type,/api/perimeter/transit/host/drive/metadata/by-slug,Built,Implied,Recipient resolves the address; everything peer-side depends on it
4 changes: 2 additions & 2 deletions src/apps/Odin.Hosting/Cli/Commands/ResetFeed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal static async Task ExecuteAsync(IServiceProvider services)

registry.LoadRegistrations().BlockingWait();
var allTenants = await registry.GetTenants();
var feedDriveId = SystemDriveConstants.FeedDrive.Alias;
var feedDriveId = WellKnownAppDrives.FeedDrive.Alias;
foreach (var tenant in allTenants)
{
var scope = tenantContainer.GetTenantScope(tenant.PrimaryDomainName);
Expand All @@ -45,7 +45,7 @@ internal static async Task ExecuteAsync(IServiceProvider services)
foreach (var header in headers)
{
// because im oddly paranoid
if (header.driveId != SystemDriveConstants.FeedDrive.Alias)
if (header.driveId != WellKnownAppDrives.FeedDrive.Alias)
{
logger.LogError("whoa horsey, you're going to delete something not on the feed " +
"drive. the incorrect drive was {d}", header.driveId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public abstract class PeerSenderControllerBase(
/// <returns></returns>
[SwaggerOperation(Tags = new[] { ControllerConstants.ClientTokenDrive })]
[HttpPost("files/send")]
public async Task<TransitResult> SendFile()
public virtual async Task<TransitResult> SendFile()
{
if (!IsMultipartContentType(HttpContext.Request.ContentType))
{
Expand Down Expand Up @@ -69,7 +69,7 @@ public async Task<TransitResult> SendFile()
/// </summary>
[SwaggerOperation(Tags = new[] { ControllerConstants.ClientTokenDrive })]
[HttpPost("files/senddeleterequest")]
public async Task<IActionResult> DeleteFile([FromBody] DeleteFileByGlobalTransitIdRequest request)
public virtual async Task<IActionResult> DeleteFile([FromBody] DeleteFileByGlobalTransitIdRequest request)
{
OdinValidationUtils.AssertNotNull(request, nameof(request));
OdinValidationUtils.AssertValidRecipientList(request?.Recipients ?? [], false);
Expand Down Expand Up @@ -156,7 +156,16 @@ await fileSystemWriter.AddThumbnail(thumbnailUploadKey, contentTypeFromMultipart
/// Map the client's transit instructions to an upload instruction set so we
/// so we can keep the upload infrastructure for Alpha
/// </summary>
private async Task<UploadInstructionSet> RemapTransitInstructionSet(Stream transitInstructionStream)
/// <summary>
/// Reads the client's <see cref="TransitInstructionSet"/> and maps it onto the transient-temp-drive
/// upload the outbox actually sends.
/// </summary>
/// <remarks>
/// Virtual so a route that names the remote drive in its path rather than its body can stamp the
/// resolved address over what the body carried -- see the slug-addressed peer write route. The
/// mapping itself stays here so both forms produce the same instruction set.
/// </remarks>
protected virtual async Task<UploadInstructionSet> RemapTransitInstructionSet(Stream transitInstructionStream)
{
string json = await new StreamReader(transitInstructionStream).ReadToEndAsync();
var transitInstructionSet = OdinSystemSerializer.Deserialize<TransitInstructionSet>(json);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Odin.Services.Drives;

Expand All @@ -10,5 +11,22 @@ public class ClientDriveData

public Dictionary<string,string> Attributes { get; set; }
public string Name { get; set; }

/// <summary>
/// The app that owns this drive; null means an owner drive.
/// </summary>
/// <remarks>
/// <see cref="AppId"/>, <see cref="DriveSlug"/> and <see cref="DriveTypeSlug"/> are columns on
/// the Drives table. Null on every drive today -- nothing derives a slug or assigns ownership
/// yet; see <c>docs/drive-addressing.md</c>.
/// </remarks>
public Guid? AppId { get; set; }

/// <summary>
/// The drive's portable name, and the readable form of its type.
/// </summary>
public string DriveSlug { get; set; }

public string DriveTypeSlug { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ public async Task<PagedResult<ClientDriveData>> GetDrivesByType([FromQuery] GetD
{
TargetDrive = drive.TargetDriveInfo,
Name = WebOdinContext.Caller.IsOwner ? drive.Name : string.Empty,
Attributes = WebOdinContext.Caller.IsOwner ? drive.Attributes : default
Attributes = WebOdinContext.Caller.IsOwner ? drive.Attributes : default,

// Redacted for third parties the same way Name and Attributes already are. The slug is
// designed to be a remote-resolvable address, but resolution happens on the recipient
// side (drive-addressing.md, "Slugs are resolved by the recipient"), so a guest does not
// need to read the list to use one.
AppId = WebOdinContext.Caller.IsOwner ? drive.AppId : null,
DriveSlug = WebOdinContext.Caller.IsOwner ? drive.DriveSlug : null,
DriveTypeSlug = WebOdinContext.Caller.IsOwner ? drive.DriveTypeSlug : null
}).ToList();

var page = new PagedResult<ClientDriveData>(drives.Request, drives.TotalPages, clientDriveData);
Expand Down
Loading
Loading