Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 0 additions & 2 deletions backend/druks/contrib/software_factory/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,6 @@ class SoftwareFactory(App):
"Turns a ticket into a pull request — it plans the change, builds it, and "
"gates on you before shipping. Reviews a pull request when asked."
)
# The requester's GitHub sign-in names them in a review.
github = services.Github.with_scopes()

class Settings(AppSettings):
tracker: Literal["none", "linear", "jira", "druks"] = Field(
Expand Down
2 changes: 1 addition & 1 deletion backend/druks/contrib/software_factory/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ async def get_prompt_context(self, **context: Any) -> dict[str, Any]:
)
# The reviewer writes on GitHub, so the requester goes by their GitHub login.
account = await Account.get_for_run(db_session(), self.account_id)
sign_ins = await SoftwareFactory.github.list_for_account(account.id)
sign_ins = await Github.list_for_account(account.id)
requested_by = sign_ins[0].identity["login"] if sign_ins else account.username
return {
"siblings": await project_repo.siblings(),
Expand Down
22 changes: 13 additions & 9 deletions backend/druks/services/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@


class Connection:
"""One signed-in provider account, reached through the app's
declared handle. ``get_access_token`` and ``disconnect`` act on this
sign-in only."""
"""One signed-in provider account. ``get_access_token`` and ``disconnect``
act on this sign-in only."""

def __init__(self, service: "type[Service]", row: VaultSecret) -> None:
self.service = service
Expand Down Expand Up @@ -108,12 +107,7 @@ def connect_url(self) -> str:
return f"/api/oauth/{self.service.slug}/connect?next=/{self.owner.name}"

async def list_for_account(self, account_id: str) -> list[Connection]:
return [
Connection(self.service, row)
for row in await VaultSecret.list_account_connections(
db_session(), Audience.service(self.service.slug), account_id
)
]
return await self.service.list_for_account(account_id)

async def get(self, connection_id: str) -> Connection | None:
row = await db_session().get(VaultSecret, connection_id)
Expand Down Expand Up @@ -278,6 +272,16 @@ def with_scopes(cls, *scopes: str) -> ScopedService:
raise TypeError(f"{cls.__name__} declares no OAuth endpoints")
return ScopedService(cls, scopes)

@classmethod
async def list_for_account(cls, account_id: str) -> list[Connection]:
"""The account's live sign-ins, without declaring an app's scope requirements."""
return [
Connection(cls, row)
for row in await VaultSecret.list_account_connections(
db_session(), Audience.service(cls.slug), account_id
)
]

@classmethod
def declarations(cls) -> "list[ScopedService]":
return [
Expand Down
13 changes: 13 additions & 0 deletions docs/writing-an-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,19 @@ for connection in await NightWatch.acme.list_for_account(account_id):
token = await connection.get_access_token()
```

Read existing sign-in facts directly from the service when you do not need to
declare scopes:

```python
from druks.core.services import Github

sign_ins = await Github.list_for_account(self.account_id)
```

Each connection's `identity["login"]` names the GitHub user. The direct read
does not add the app to the service's scope declarations. An empty list means
the account has no live sign-in. Both reads exclude revoked connections.

`account_id` is the caller: `self.account_id` in a run body,
`current_account_id.get()` in a route, the handler's argument in a
subscriber, the platform's argument in `list_summaries`. `await NightWatch.acme.get(connection_id)` returns one connection
Expand Down
Loading