From 1caa4e580c6dc135c97bb158051c707539dac46b Mon Sep 17 00:00:00 2001 From: Jasper Van Date: Sat, 1 Aug 2026 00:32:31 -0400 Subject: [PATCH] refactor(oauth): replace agent access naming (#549) --- CLAUDE.md | 2 +- V2_ROADMAP.md | 2 +- cmd/internal/openapi/client.gen.go | 1448 ++++++++--------- ...gent-authentication.md => oauth-server.md} | 21 +- docs/roadmap/v2.9.md | 13 +- ...gent-access.spec.ts => oauth-apps.spec.ts} | 28 +- package.json | 2 +- ...uth-scopes.ts => backfill-oauth-scopes.ts} | 56 +- server/adapters/repos/audit.ts | 6 +- .../{agent-oauth.test.ts => oauth.test.ts} | 16 +- .../repos/{agent-oauth.ts => oauth.ts} | 6 +- server/adapters/repos/org.test.ts | 16 - server/adapters/repos/org.ts | 8 +- server/app.ts | 16 +- server/auth.integration.test.ts | 26 +- server/auth.ts | 12 +- ...rovider.test.ts => oauth-provider.test.ts} | 22 +- ...nt-oauth-provider.ts => oauth-provider.ts} | 56 +- server/composition.ts | 4 +- server/db/auth-schema.test.ts | 2 +- server/http/auth/auth.cf-test.ts | 12 +- ...st.ts => oauth-grants.integration.test.ts} | 22 +- ...{agent-oauth-grants.ts => oauth-grants.ts} | 74 +- server/http/oauth-resource-scopes.ts | 8 +- server/http/openapi.ts | 6 +- server/http/site/audit.integration.test.ts | 25 + server/http/site/audit.ts | 2 +- server/http/teams.ts | 2 +- server/middleware/audit-actor.test.ts | 6 +- server/middleware/audit-actor.ts | 4 +- server/middleware/auth.ts | 8 +- server/middleware/platform.ts | 6 +- server/openapi.test.ts | 8 +- ....test.ts => backfill-oauth-scopes.test.ts} | 50 +- server/usecases/deps.ts | 4 +- ...-consent.test.ts => oauth-consent.test.ts} | 39 +- ...gent-oauth-consent.ts => oauth-consent.ts} | 32 +- ...th-grants.test.ts => oauth-grants.test.ts} | 21 +- ...{agent-oauth-grants.ts => oauth-grants.ts} | 22 +- server/usecases/ports.ts | 2 +- server/usecases/ports/audit.ts | 2 +- .../ports/{agent-oauth.ts => oauth.ts} | 10 +- server/usecases/ports/org.ts | 1 - server/usecases/site/auth-provider.test.ts | 6 +- server/usecases/site/auth-provider.ts | 6 +- server/usecases/team.test.ts | 1 - shared/authorization.test.ts | 4 +- shared/authorization.ts | 6 +- shared/{agent-oauth.ts => oauth.ts} | 14 +- shared/schemas/agent-oauth-grants.ts | 64 - shared/schemas/index.ts | 38 +- shared/schemas/oauth-grants.ts | 64 + shared/schemas/oauth-resource.ts | 24 +- shared/types/index.ts | 2 +- src/components/admin/audit-activity-feed.tsx | 2 +- src/i18n/locales/en.json | 82 +- src/i18n/locales/zh.json | 82 +- src/lib/api.test.ts | 32 +- src/lib/api.ts | 30 +- src/lib/rpc.ts | 4 +- src/routeTree.gen.ts | 45 +- .../_authenticated/admin/audit.test.tsx | 2 +- src/routes/_authenticated/admin/audit.tsx | 2 +- .../{agent-access.tsx => oauth-apps.tsx} | 88 +- src/routes/_authenticated/settings/route.tsx | 2 +- .../_authenticated/teams/$teamId/activity.tsx | 2 +- 66 files changed, 1356 insertions(+), 1374 deletions(-) rename docs/design/{agent-authentication.md => oauth-server.md} (94%) rename e2e/{agent-access.spec.ts => oauth-apps.spec.ts} (87%) rename scripts/{backfill-agent-oauth-scopes.ts => backfill-oauth-scopes.ts} (74%) rename server/adapters/repos/{agent-oauth.test.ts => oauth.test.ts} (90%) rename server/adapters/repos/{agent-oauth.ts => oauth.ts} (97%) rename server/auth/{agent-oauth-provider.test.ts => oauth-provider.test.ts} (85%) rename server/auth/{agent-oauth-provider.ts => oauth-provider.ts} (86%) rename server/http/{agent-oauth-grants.integration.test.ts => oauth-grants.integration.test.ts} (87%) rename server/http/{agent-oauth-grants.ts => oauth-grants.ts} (55%) rename server/scripts/{backfill-agent-oauth-scopes.test.ts => backfill-oauth-scopes.test.ts} (72%) rename server/usecases/{agent-oauth-consent.test.ts => oauth-consent.test.ts} (78%) rename server/usecases/{agent-oauth-consent.ts => oauth-consent.ts} (63%) rename server/usecases/{agent-oauth-grants.test.ts => oauth-grants.test.ts} (67%) rename server/usecases/{agent-oauth-grants.ts => oauth-grants.ts} (63%) rename server/usecases/ports/{agent-oauth.ts => oauth.ts} (78%) rename shared/{agent-oauth.ts => oauth.ts} (72%) delete mode 100644 shared/schemas/agent-oauth-grants.ts create mode 100644 shared/schemas/oauth-grants.ts rename src/routes/_authenticated/settings/{agent-access.tsx => oauth-apps.tsx} (78%) diff --git a/CLAUDE.md b/CLAUDE.md index 987dbea2..430d7cd2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ Core architecture: clients upload directly to S3-compatible storage via presigne - [docs/architecture.md](docs/architecture.md) — system architecture, tech decisions, platform abstraction - [docs/design/admin-form-ui.md](docs/design/admin-form-ui.md) — admin form layout, density, required/help/placeholder rules - [docs/design/upload-policies.md](docs/design/upload-policies.md) — proposed selector-based upload placement policies for multiple storage backends -- [docs/design/agent-authentication.md](docs/design/agent-authentication.md) — Agent OAuth, CI API keys, protocol-neutral authorization, Restish credential handling, and future Agent Auth compatibility +- [docs/design/oauth-server.md](docs/design/oauth-server.md) — OAuth, CI API keys, protocol-neutral authorization, Restish credential handling, and future Agent Auth compatibility - [V2_ROADMAP.md](V2_ROADMAP.md) — product positioning, release plan (v2.0–v2.9) - [docs/roadmap/](docs/roadmap/) — per-version technical specs (v2.0.md–v2.9.md) - [docs/design/spaces-quota-sharing.md](docs/design/spaces-quota-sharing.md) — spaces/quota/sharing design decisions (team billing, allocation, cross-space transfer, no per-item ACL) diff --git a/V2_ROADMAP.md b/V2_ROADMAP.md index 3fcc8639..fd1e8c14 100644 --- a/V2_ROADMAP.md +++ b/V2_ROADMAP.md @@ -37,7 +37,7 @@ Each version ships 1–2 major features. Ship small, ship often. | [v2.6](docs/roadmap/v2.6.md) | **Pro / Business Launch** | Cloud binding, entitlement system, white-label, audit log, quota store machinery, site announcements, retroactive gates | | [v2.7](docs/roadmap/v2.7.md) | **WebDAV & File Processing** | WebDAV protocol access, small-file zip compression/extraction, and Community remote-download orchestration through Aria2, qBittorrent, and future adapters | | [v2.8](docs/roadmap/v2.8.md) | **Admin Analytics & Dashboard** | Admin overview, usage/cost/reliability metrics, share analytics, and offline result coverage | -| [v2.9](docs/roadmap/v2.9.md) | **Agent Access** | Unified OpenAPI + Restish + upload plugin + Skill, protocol-neutral authorization, delegated OAuth, and workspace-scoped CI keys | +| [v2.9](docs/roadmap/v2.9.md) | **External OAuth Access** | Self-describing OpenAPI and upload workflows, delegated OAuth, DPoP, and protocol-neutral authorization | | [v2.10](docs/roadmap/v2.10.md) | **Desktop Sync Foundation** | Sync device model, change log, sync API contract, conflict model, and protocol tests for external clients | | Future | **Native Client Projects** | macOS File Provider, Windows/Linux sync clients, Flutter/mobile clients, and other OS-specific apps in separate repositories | | Future | **Enterprise Identity & Governance** | SAML, LDAP / SCIM, advanced RBAC/custom roles, retention, and admin support mode if demand proves real | diff --git a/cmd/internal/openapi/client.gen.go b/cmd/internal/openapi/client.gen.go index 4591d78a..447aa30e 100644 --- a/cmd/internal/openapi/client.gen.go +++ b/cmd/internal/openapi/client.gen.go @@ -1069,10 +1069,10 @@ func (e AdminDashboardDownloadersItemsStatus) Valid() bool { // Defines values for AuditEventActorType. const ( AuditEventActorTypeAgent AuditEventActorType = "agent" - AuditEventActorTypeAgentOauth AuditEventActorType = "agent_oauth" AuditEventActorTypeAnonymous AuditEventActorType = "anonymous" AuditEventActorTypeApiKey AuditEventActorType = "api_key" AuditEventActorTypeDownloader AuditEventActorType = "downloader" + AuditEventActorTypeOauth AuditEventActorType = "oauth" AuditEventActorTypeSystem AuditEventActorType = "system" AuditEventActorTypeTaskUpload AuditEventActorType = "task-upload" AuditEventActorTypeUser AuditEventActorType = "user" @@ -1083,14 +1083,14 @@ func (e AuditEventActorType) Valid() bool { switch e { case AuditEventActorTypeAgent: return true - case AuditEventActorTypeAgentOauth: - return true case AuditEventActorTypeAnonymous: return true case AuditEventActorTypeApiKey: return true case AuditEventActorTypeDownloader: return true + case AuditEventActorTypeOauth: + return true case AuditEventActorTypeSystem: return true case AuditEventActorTypeTaskUpload: @@ -2044,105 +2044,6 @@ func (e WebDavVerificationStatus) Valid() bool { } } -// Defines values for GetAgentOAuthConsentContext200JSONResponseBodyScopes. -const ( - GetAgentOAuthConsentContext200JSONResponseBodyScopesObjectsCreate GetAgentOAuthConsentContext200JSONResponseBodyScopes = "objects:create" - GetAgentOAuthConsentContext200JSONResponseBodyScopesObjectsDelete GetAgentOAuthConsentContext200JSONResponseBodyScopes = "objects:delete" - GetAgentOAuthConsentContext200JSONResponseBodyScopesObjectsRead GetAgentOAuthConsentContext200JSONResponseBodyScopes = "objects:read" - GetAgentOAuthConsentContext200JSONResponseBodyScopesObjectsUpdate GetAgentOAuthConsentContext200JSONResponseBodyScopes = "objects:update" - GetAgentOAuthConsentContext200JSONResponseBodyScopesQuotaPurchase GetAgentOAuthConsentContext200JSONResponseBodyScopes = "quota:purchase" - GetAgentOAuthConsentContext200JSONResponseBodyScopesQuotaRead GetAgentOAuthConsentContext200JSONResponseBodyScopes = "quota:read" - GetAgentOAuthConsentContext200JSONResponseBodyScopesSharesCreate GetAgentOAuthConsentContext200JSONResponseBodyScopes = "shares:create" - GetAgentOAuthConsentContext200JSONResponseBodyScopesSharesDelete GetAgentOAuthConsentContext200JSONResponseBodyScopes = "shares:delete" - GetAgentOAuthConsentContext200JSONResponseBodyScopesSharesRead GetAgentOAuthConsentContext200JSONResponseBodyScopes = "shares:read" - GetAgentOAuthConsentContext200JSONResponseBodyScopesStorageUsageRead GetAgentOAuthConsentContext200JSONResponseBodyScopes = "storage-usage:read" -) - -// Valid indicates whether the value is a known member of the GetAgentOAuthConsentContext200JSONResponseBodyScopes enum. -func (e GetAgentOAuthConsentContext200JSONResponseBodyScopes) Valid() bool { - switch e { - case GetAgentOAuthConsentContext200JSONResponseBodyScopesObjectsCreate: - return true - case GetAgentOAuthConsentContext200JSONResponseBodyScopesObjectsDelete: - return true - case GetAgentOAuthConsentContext200JSONResponseBodyScopesObjectsRead: - return true - case GetAgentOAuthConsentContext200JSONResponseBodyScopesObjectsUpdate: - return true - case GetAgentOAuthConsentContext200JSONResponseBodyScopesQuotaPurchase: - return true - case GetAgentOAuthConsentContext200JSONResponseBodyScopesQuotaRead: - return true - case GetAgentOAuthConsentContext200JSONResponseBodyScopesSharesCreate: - return true - case GetAgentOAuthConsentContext200JSONResponseBodyScopesSharesDelete: - return true - case GetAgentOAuthConsentContext200JSONResponseBodyScopesSharesRead: - return true - case GetAgentOAuthConsentContext200JSONResponseBodyScopesStorageUsageRead: - return true - default: - return false - } -} - -// Defines values for ListAgentOAuthGrants200JSONResponseBodyItemsScopes. -const ( - ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsCreate ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:create" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsDelete ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:delete" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsRead ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:read" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsUpdate ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:update" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesQuotaPurchase ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "quota:purchase" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesQuotaRead ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "quota:read" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesSharesCreate ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "shares:create" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesSharesDelete ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "shares:delete" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesSharesRead ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "shares:read" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesStorageUsageRead ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "storage-usage:read" -) - -// Valid indicates whether the value is a known member of the ListAgentOAuthGrants200JSONResponseBodyItemsScopes enum. -func (e ListAgentOAuthGrants200JSONResponseBodyItemsScopes) Valid() bool { - switch e { - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsCreate: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsDelete: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsRead: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsUpdate: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesQuotaPurchase: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesQuotaRead: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesSharesCreate: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesSharesDelete: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesSharesRead: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesStorageUsageRead: - return true - default: - return false - } -} - -// Defines values for ListAgentOAuthGrants200JSONResponseBodyItemsStatus. -const ( - ListAgentOAuthGrants200JSONResponseBodyItemsStatusActive ListAgentOAuthGrants200JSONResponseBodyItemsStatus = "active" -) - -// Valid indicates whether the value is a known member of the ListAgentOAuthGrants200JSONResponseBodyItemsStatus enum. -func (e ListAgentOAuthGrants200JSONResponseBodyItemsStatus) Valid() bool { - switch e { - case ListAgentOAuthGrants200JSONResponseBodyItemsStatusActive: - return true - default: - return false - } -} - // Defines values for ListUsersParamsSearchField. const ( ListUsersParamsSearchFieldEmail ListUsersParamsSearchField = "email" @@ -3154,6 +3055,105 @@ func (e PresignImageHostingUploadJSONBodyMime) Valid() bool { } } +// Defines values for GetOAuthConsentContext200JSONResponseBodyScopes. +const ( + GetOAuthConsentContext200JSONResponseBodyScopesObjectsCreate GetOAuthConsentContext200JSONResponseBodyScopes = "objects:create" + GetOAuthConsentContext200JSONResponseBodyScopesObjectsDelete GetOAuthConsentContext200JSONResponseBodyScopes = "objects:delete" + GetOAuthConsentContext200JSONResponseBodyScopesObjectsRead GetOAuthConsentContext200JSONResponseBodyScopes = "objects:read" + GetOAuthConsentContext200JSONResponseBodyScopesObjectsUpdate GetOAuthConsentContext200JSONResponseBodyScopes = "objects:update" + GetOAuthConsentContext200JSONResponseBodyScopesQuotaPurchase GetOAuthConsentContext200JSONResponseBodyScopes = "quota:purchase" + GetOAuthConsentContext200JSONResponseBodyScopesQuotaRead GetOAuthConsentContext200JSONResponseBodyScopes = "quota:read" + GetOAuthConsentContext200JSONResponseBodyScopesSharesCreate GetOAuthConsentContext200JSONResponseBodyScopes = "shares:create" + GetOAuthConsentContext200JSONResponseBodyScopesSharesDelete GetOAuthConsentContext200JSONResponseBodyScopes = "shares:delete" + GetOAuthConsentContext200JSONResponseBodyScopesSharesRead GetOAuthConsentContext200JSONResponseBodyScopes = "shares:read" + GetOAuthConsentContext200JSONResponseBodyScopesStorageUsageRead GetOAuthConsentContext200JSONResponseBodyScopes = "storage-usage:read" +) + +// Valid indicates whether the value is a known member of the GetOAuthConsentContext200JSONResponseBodyScopes enum. +func (e GetOAuthConsentContext200JSONResponseBodyScopes) Valid() bool { + switch e { + case GetOAuthConsentContext200JSONResponseBodyScopesObjectsCreate: + return true + case GetOAuthConsentContext200JSONResponseBodyScopesObjectsDelete: + return true + case GetOAuthConsentContext200JSONResponseBodyScopesObjectsRead: + return true + case GetOAuthConsentContext200JSONResponseBodyScopesObjectsUpdate: + return true + case GetOAuthConsentContext200JSONResponseBodyScopesQuotaPurchase: + return true + case GetOAuthConsentContext200JSONResponseBodyScopesQuotaRead: + return true + case GetOAuthConsentContext200JSONResponseBodyScopesSharesCreate: + return true + case GetOAuthConsentContext200JSONResponseBodyScopesSharesDelete: + return true + case GetOAuthConsentContext200JSONResponseBodyScopesSharesRead: + return true + case GetOAuthConsentContext200JSONResponseBodyScopesStorageUsageRead: + return true + default: + return false + } +} + +// Defines values for ListOAuthGrants200JSONResponseBodyItemsScopes. +const ( + ListOAuthGrants200JSONResponseBodyItemsScopesObjectsCreate ListOAuthGrants200JSONResponseBodyItemsScopes = "objects:create" + ListOAuthGrants200JSONResponseBodyItemsScopesObjectsDelete ListOAuthGrants200JSONResponseBodyItemsScopes = "objects:delete" + ListOAuthGrants200JSONResponseBodyItemsScopesObjectsRead ListOAuthGrants200JSONResponseBodyItemsScopes = "objects:read" + ListOAuthGrants200JSONResponseBodyItemsScopesObjectsUpdate ListOAuthGrants200JSONResponseBodyItemsScopes = "objects:update" + ListOAuthGrants200JSONResponseBodyItemsScopesQuotaPurchase ListOAuthGrants200JSONResponseBodyItemsScopes = "quota:purchase" + ListOAuthGrants200JSONResponseBodyItemsScopesQuotaRead ListOAuthGrants200JSONResponseBodyItemsScopes = "quota:read" + ListOAuthGrants200JSONResponseBodyItemsScopesSharesCreate ListOAuthGrants200JSONResponseBodyItemsScopes = "shares:create" + ListOAuthGrants200JSONResponseBodyItemsScopesSharesDelete ListOAuthGrants200JSONResponseBodyItemsScopes = "shares:delete" + ListOAuthGrants200JSONResponseBodyItemsScopesSharesRead ListOAuthGrants200JSONResponseBodyItemsScopes = "shares:read" + ListOAuthGrants200JSONResponseBodyItemsScopesStorageUsageRead ListOAuthGrants200JSONResponseBodyItemsScopes = "storage-usage:read" +) + +// Valid indicates whether the value is a known member of the ListOAuthGrants200JSONResponseBodyItemsScopes enum. +func (e ListOAuthGrants200JSONResponseBodyItemsScopes) Valid() bool { + switch e { + case ListOAuthGrants200JSONResponseBodyItemsScopesObjectsCreate: + return true + case ListOAuthGrants200JSONResponseBodyItemsScopesObjectsDelete: + return true + case ListOAuthGrants200JSONResponseBodyItemsScopesObjectsRead: + return true + case ListOAuthGrants200JSONResponseBodyItemsScopesObjectsUpdate: + return true + case ListOAuthGrants200JSONResponseBodyItemsScopesQuotaPurchase: + return true + case ListOAuthGrants200JSONResponseBodyItemsScopesQuotaRead: + return true + case ListOAuthGrants200JSONResponseBodyItemsScopesSharesCreate: + return true + case ListOAuthGrants200JSONResponseBodyItemsScopesSharesDelete: + return true + case ListOAuthGrants200JSONResponseBodyItemsScopesSharesRead: + return true + case ListOAuthGrants200JSONResponseBodyItemsScopesStorageUsageRead: + return true + default: + return false + } +} + +// Defines values for ListOAuthGrants200JSONResponseBodyItemsStatus. +const ( + ListOAuthGrants200JSONResponseBodyItemsStatusActive ListOAuthGrants200JSONResponseBodyItemsStatus = "active" +) + +// Valid indicates whether the value is a known member of the ListOAuthGrants200JSONResponseBodyItemsStatus enum. +func (e ListOAuthGrants200JSONResponseBodyItemsStatus) Valid() bool { + switch e { + case ListOAuthGrants200JSONResponseBodyItemsStatusActive: + return true + default: + return false + } +} + // Defines values for CreateObjectJSONBodyOnConflict. const ( CreateObjectJSONBodyOnConflictFail CreateObjectJSONBodyOnConflict = "fail" @@ -6370,26 +6370,6 @@ type User struct { // WebDavVerificationStatus defines model for WebDavVerificationStatus. type WebDavVerificationStatus string -// GetAgentOAuthConsentContextParams defines parameters for GetAgentOAuthConsentContext. -type GetAgentOAuthConsentContextParams struct { - OauthQuery string `form:"oauthQuery" json:"oauthQuery"` -} - -// GetAgentOAuthConsentContext200JSONResponseBodyScopes defines parameters for GetAgentOAuthConsentContext. -type GetAgentOAuthConsentContext200JSONResponseBodyScopes string - -// SubmitAgentOAuthConsentJSONBody defines parameters for SubmitAgentOAuthConsent. -type SubmitAgentOAuthConsentJSONBody struct { - Accept bool `json:"accept"` - OauthQuery string `json:"oauthQuery"` -} - -// ListAgentOAuthGrants200JSONResponseBodyItemsScopes defines parameters for ListAgentOAuthGrants. -type ListAgentOAuthGrants200JSONResponseBodyItemsScopes string - -// ListAgentOAuthGrants200JSONResponseBodyItemsStatus defines parameters for ListAgentOAuthGrants. -type ListAgentOAuthGrants200JSONResponseBodyItemsStatus string - // BanUserJSONBody defines parameters for BanUser. type BanUserJSONBody struct { // BanExpiresIn The number of seconds until the ban expires @@ -7936,6 +7916,26 @@ type ListNotificationsParams struct { Unread *string `form:"unread,omitempty" json:"unread,omitempty"` } +// GetOAuthConsentContextParams defines parameters for GetOAuthConsentContext. +type GetOAuthConsentContextParams struct { + OauthQuery string `form:"oauthQuery" json:"oauthQuery"` +} + +// GetOAuthConsentContext200JSONResponseBodyScopes defines parameters for GetOAuthConsentContext. +type GetOAuthConsentContext200JSONResponseBodyScopes string + +// SubmitOAuthConsentJSONBody defines parameters for SubmitOAuthConsent. +type SubmitOAuthConsentJSONBody struct { + Accept bool `json:"accept"` + OauthQuery string `json:"oauthQuery"` +} + +// ListOAuthGrants200JSONResponseBodyItemsScopes defines parameters for ListOAuthGrants. +type ListOAuthGrants200JSONResponseBodyItemsScopes string + +// ListOAuthGrants200JSONResponseBodyItemsStatus defines parameters for ListOAuthGrants. +type ListOAuthGrants200JSONResponseBodyItemsStatus string + // ListObjectsParams defines parameters for ListObjects. type ListObjectsParams struct { PageSize *int `form:"pageSize,omitempty" json:"pageSize,omitempty"` @@ -8485,9 +8485,6 @@ type UpdateUserEntitlementJSONBody struct { Note *string `json:"note,omitempty"` } -// SubmitAgentOAuthConsentJSONRequestBody defines body for SubmitAgentOAuthConsent for application/json ContentType. -type SubmitAgentOAuthConsentJSONRequestBody SubmitAgentOAuthConsentJSONBody - // BanUserJSONRequestBody defines body for BanUser for application/json ContentType. type BanUserJSONRequestBody BanUserJSONBody @@ -8734,6 +8731,9 @@ type UpdateImageHostingConfigJSONRequestBody UpdateImageHostingConfigJSONBody // PresignImageHostingUploadJSONRequestBody defines body for PresignImageHostingUpload for application/json ContentType. type PresignImageHostingUploadJSONRequestBody PresignImageHostingUploadJSONBody +// SubmitOAuthConsentJSONRequestBody defines body for SubmitOAuthConsent for application/json ContentType. +type SubmitOAuthConsentJSONRequestBody SubmitOAuthConsentJSONBody + // CreateObjectJSONRequestBody defines body for CreateObject for application/json ContentType. type CreateObjectJSONRequestBody CreateObjectJSONBody @@ -10518,20 +10518,6 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { - // GetAgentOAuthConsentContext request - GetAgentOAuthConsentContext(ctx context.Context, params *GetAgentOAuthConsentContextParams, reqEditors ...RequestEditorFn) (*http.Response, error) - - // SubmitAgentOAuthConsentWithBody request with any body - SubmitAgentOAuthConsentWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - SubmitAgentOAuthConsent(ctx context.Context, body SubmitAgentOAuthConsentJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // ListAgentOAuthGrants request - ListAgentOAuthGrants(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) - - // RevokeAgentOAuthGrant request - RevokeAgentOAuthGrant(ctx context.Context, grantId string, reqEditors ...RequestEditorFn) (*http.Response, error) - // GetApiAuthAccountInfo request GetApiAuthAccountInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -11119,6 +11105,20 @@ type ClientInterface interface { // MarkNotificationRead request MarkNotificationRead(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetOAuthConsentContext request + GetOAuthConsentContext(ctx context.Context, params *GetOAuthConsentContextParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // SubmitOAuthConsentWithBody request with any body + SubmitOAuthConsentWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + SubmitOAuthConsent(ctx context.Context, body SubmitOAuthConsentJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ListOAuthGrants request + ListOAuthGrants(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // RevokeOAuthGrant request + RevokeOAuthGrant(ctx context.Context, grantId string, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListOAuthResourceScopes request ListOAuthResourceScopes(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -11553,66 +11553,6 @@ type ClientInterface interface { GetUserProfile(ctx context.Context, username string, reqEditors ...RequestEditorFn) (*http.Response, error) } -func (c *Client) GetAgentOAuthConsentContext(ctx context.Context, params *GetAgentOAuthConsentContextParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetAgentOAuthConsentContextRequest(c.Server, params) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) SubmitAgentOAuthConsentWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewSubmitAgentOAuthConsentRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) SubmitAgentOAuthConsent(ctx context.Context, body SubmitAgentOAuthConsentJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewSubmitAgentOAuthConsentRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) ListAgentOAuthGrants(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewListAgentOAuthGrantsRequest(c.Server) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) RevokeAgentOAuthGrant(ctx context.Context, grantId string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewRevokeAgentOAuthGrantRequest(c.Server, grantId) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - func (c *Client) GetApiAuthAccountInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGetApiAuthAccountInfoRequest(c.Server) if err != nil { @@ -14289,6 +14229,66 @@ func (c *Client) MarkNotificationRead(ctx context.Context, id string, reqEditors return c.Client.Do(req) } +func (c *Client) GetOAuthConsentContext(ctx context.Context, params *GetOAuthConsentContextParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetOAuthConsentContextRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) SubmitOAuthConsentWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSubmitOAuthConsentRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) SubmitOAuthConsent(ctx context.Context, body SubmitOAuthConsentJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSubmitOAuthConsentRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ListOAuthGrants(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListOAuthGrantsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) RevokeOAuthGrant(ctx context.Context, grantId string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRevokeOAuthGrantRequest(c.Server, grantId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) ListOAuthResourceScopes(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewListOAuthResourceScopesRequest(c.Server) if err != nil { @@ -16185,157 +16185,6 @@ func (c *Client) GetUserProfile(ctx context.Context, username string, reqEditors return c.Client.Do(req) } -// NewGetAgentOAuthConsentContextRequest generates requests for GetAgentOAuthConsentContext -func NewGetAgentOAuthConsentContextRequest(server string, params *GetAgentOAuthConsentContextParams) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/api/agent-oauth-consent") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "oauthQuery", params.OauthQuery, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewSubmitAgentOAuthConsentRequest calls the generic SubmitAgentOAuthConsent builder with application/json body -func NewSubmitAgentOAuthConsentRequest(server string, body SubmitAgentOAuthConsentJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewSubmitAgentOAuthConsentRequestWithBody(server, "application/json", bodyReader) -} - -// NewSubmitAgentOAuthConsentRequestWithBody generates requests for SubmitAgentOAuthConsent with any type of body -func NewSubmitAgentOAuthConsentRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/api/agent-oauth-consent") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewListAgentOAuthGrantsRequest generates requests for ListAgentOAuthGrants -func NewListAgentOAuthGrantsRequest(server string) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/api/agent-oauth-grants") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewRevokeAgentOAuthGrantRequest generates requests for RevokeAgentOAuthGrant -func NewRevokeAgentOAuthGrantRequest(server string, grantId string) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "grantId", grantId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/api/agent-oauth-grants/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - // NewGetApiAuthAccountInfoRequest generates requests for GetApiAuthAccountInfo func NewGetApiAuthAccountInfoRequest(server string) (*http.Request, error) { var err error @@ -22530,6 +22379,157 @@ func NewMarkNotificationReadRequest(server string, id string) (*http.Request, er return req, nil } +// NewGetOAuthConsentContextRequest generates requests for GetOAuthConsentContext +func NewGetOAuthConsentContextRequest(server string, params *GetOAuthConsentContextParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/oauth-consent") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). + queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "oauthQuery", params.OauthQuery, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewSubmitOAuthConsentRequest calls the generic SubmitOAuthConsent builder with application/json body +func NewSubmitOAuthConsentRequest(server string, body SubmitOAuthConsentJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewSubmitOAuthConsentRequestWithBody(server, "application/json", bodyReader) +} + +// NewSubmitOAuthConsentRequestWithBody generates requests for SubmitOAuthConsent with any type of body +func NewSubmitOAuthConsentRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/oauth-consent") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewListOAuthGrantsRequest generates requests for ListOAuthGrants +func NewListOAuthGrantsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/oauth-grants") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewRevokeOAuthGrantRequest generates requests for RevokeOAuthGrant +func NewRevokeOAuthGrantRequest(server string, grantId string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "grantId", grantId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/oauth-grants/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewListOAuthResourceScopesRequest generates requests for ListOAuthResourceScopes func NewListOAuthResourceScopesRequest(server string) (*http.Request, error) { var err error @@ -27727,20 +27727,6 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { - // GetAgentOAuthConsentContextWithResponse request - GetAgentOAuthConsentContextWithResponse(ctx context.Context, params *GetAgentOAuthConsentContextParams, reqEditors ...RequestEditorFn) (*GetAgentOAuthConsentContextResponse, error) - - // SubmitAgentOAuthConsentWithBodyWithResponse request with any body - SubmitAgentOAuthConsentWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SubmitAgentOAuthConsentResponse, error) - - SubmitAgentOAuthConsentWithResponse(ctx context.Context, body SubmitAgentOAuthConsentJSONRequestBody, reqEditors ...RequestEditorFn) (*SubmitAgentOAuthConsentResponse, error) - - // ListAgentOAuthGrantsWithResponse request - ListAgentOAuthGrantsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListAgentOAuthGrantsResponse, error) - - // RevokeAgentOAuthGrantWithResponse request - RevokeAgentOAuthGrantWithResponse(ctx context.Context, grantId string, reqEditors ...RequestEditorFn) (*RevokeAgentOAuthGrantResponse, error) - // GetApiAuthAccountInfoWithResponse request GetApiAuthAccountInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiAuthAccountInfoResponse, error) @@ -28328,6 +28314,20 @@ type ClientWithResponsesInterface interface { // MarkNotificationReadWithResponse request MarkNotificationReadWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*MarkNotificationReadResponse, error) + // GetOAuthConsentContextWithResponse request + GetOAuthConsentContextWithResponse(ctx context.Context, params *GetOAuthConsentContextParams, reqEditors ...RequestEditorFn) (*GetOAuthConsentContextResponse, error) + + // SubmitOAuthConsentWithBodyWithResponse request with any body + SubmitOAuthConsentWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SubmitOAuthConsentResponse, error) + + SubmitOAuthConsentWithResponse(ctx context.Context, body SubmitOAuthConsentJSONRequestBody, reqEditors ...RequestEditorFn) (*SubmitOAuthConsentResponse, error) + + // ListOAuthGrantsWithResponse request + ListOAuthGrantsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListOAuthGrantsResponse, error) + + // RevokeOAuthGrantWithResponse request + RevokeOAuthGrantWithResponse(ctx context.Context, grantId string, reqEditors ...RequestEditorFn) (*RevokeOAuthGrantResponse, error) + // ListOAuthResourceScopesWithResponse request ListOAuthResourceScopesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListOAuthResourceScopesResponse, error) @@ -28762,160 +28762,6 @@ type ClientWithResponsesInterface interface { GetUserProfileWithResponse(ctx context.Context, username string, reqEditors ...RequestEditorFn) (*GetUserProfileResponse, error) } -type GetAgentOAuthConsentContextResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *struct { - ClientId string `json:"clientId"` - ClientName string `json:"clientName"` - GrantLifetime struct { - AccessTokenSeconds *int `json:"accessTokenSeconds,omitempty"` - RefreshTokenSeconds *int `json:"refreshTokenSeconds,omitempty"` - } `json:"grantLifetime"` - InstanceOrigin string `json:"instanceOrigin"` - RedirectUri string `json:"redirectUri"` - Scopes []GetAgentOAuthConsentContext200JSONResponseBodyScopes `json:"scopes"` - StandardScopes []string `json:"standardScopes"` - Workspace struct { - Id string `json:"id"` - Name *string `json:"name"` - } `json:"workspace"` - } - JSON400 *Error - JSON403 *Error -} - -// Status returns HTTPResponse.Status -func (r GetAgentOAuthConsentContextResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r GetAgentOAuthConsentContextResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r GetAgentOAuthConsentContextResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type SubmitAgentOAuthConsentResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *struct { - Url string `json:"url"` - } - JSON400 *Error - JSON403 *Error -} - -// Status returns HTTPResponse.Status -func (r SubmitAgentOAuthConsentResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r SubmitAgentOAuthConsentResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r SubmitAgentOAuthConsentResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type ListAgentOAuthGrantsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *struct { - Items []struct { - ClientId string `json:"clientId"` - ClientName string `json:"clientName"` - CreatedAt string `json:"createdAt"` - Id string `json:"id"` - LastUsedAt *string `json:"lastUsedAt"` - OrgId string `json:"orgId"` - Scopes []ListAgentOAuthGrants200JSONResponseBodyItemsScopes `json:"scopes"` - Status ListAgentOAuthGrants200JSONResponseBodyItemsStatus `json:"status"` - UserId string `json:"userId"` - WorkspaceName *string `json:"workspaceName"` - } `json:"items"` - } -} - -// Status returns HTTPResponse.Status -func (r ListAgentOAuthGrantsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r ListAgentOAuthGrantsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r ListAgentOAuthGrantsResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - -type RevokeAgentOAuthGrantResponse struct { - Body []byte - HTTPResponse *http.Response - JSON404 *Error -} - -// Status returns HTTPResponse.Status -func (r RevokeAgentOAuthGrantResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r RevokeAgentOAuthGrantResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r RevokeAgentOAuthGrantResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - type GetApiAuthAccountInfoResponse struct { Body []byte HTTPResponse *http.Response @@ -35969,6 +35815,160 @@ func (r MarkNotificationReadResponse) ContentType() string { return "" } +type GetOAuthConsentContextResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + ClientId string `json:"clientId"` + ClientName string `json:"clientName"` + GrantLifetime struct { + AccessTokenSeconds *int `json:"accessTokenSeconds,omitempty"` + RefreshTokenSeconds *int `json:"refreshTokenSeconds,omitempty"` + } `json:"grantLifetime"` + InstanceOrigin string `json:"instanceOrigin"` + RedirectUri string `json:"redirectUri"` + Scopes []GetOAuthConsentContext200JSONResponseBodyScopes `json:"scopes"` + StandardScopes []string `json:"standardScopes"` + Workspace struct { + Id string `json:"id"` + Name *string `json:"name"` + } `json:"workspace"` + } + JSON400 *Error + JSON403 *Error +} + +// Status returns HTTPResponse.Status +func (r GetOAuthConsentContextResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetOAuthConsentContextResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r GetOAuthConsentContextResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type SubmitOAuthConsentResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + Url string `json:"url"` + } + JSON400 *Error + JSON403 *Error +} + +// Status returns HTTPResponse.Status +func (r SubmitOAuthConsentResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r SubmitOAuthConsentResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r SubmitOAuthConsentResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type ListOAuthGrantsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + Items []struct { + ClientId string `json:"clientId"` + ClientName string `json:"clientName"` + CreatedAt string `json:"createdAt"` + Id string `json:"id"` + LastUsedAt *string `json:"lastUsedAt"` + OrgId string `json:"orgId"` + Scopes []ListOAuthGrants200JSONResponseBodyItemsScopes `json:"scopes"` + Status ListOAuthGrants200JSONResponseBodyItemsStatus `json:"status"` + UserId string `json:"userId"` + WorkspaceName *string `json:"workspaceName"` + } `json:"items"` + } +} + +// Status returns HTTPResponse.Status +func (r ListOAuthGrantsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListOAuthGrantsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r ListOAuthGrantsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type RevokeOAuthGrantResponse struct { + Body []byte + HTTPResponse *http.Response + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r RevokeOAuthGrantResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RevokeOAuthGrantResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r RevokeOAuthGrantResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type ListOAuthResourceScopesResponse struct { Body []byte HTTPResponse *http.Response @@ -39794,50 +39794,6 @@ func (r GetUserProfileResponse) ContentType() string { return "" } -// GetAgentOAuthConsentContextWithResponse request returning *GetAgentOAuthConsentContextResponse -func (c *ClientWithResponses) GetAgentOAuthConsentContextWithResponse(ctx context.Context, params *GetAgentOAuthConsentContextParams, reqEditors ...RequestEditorFn) (*GetAgentOAuthConsentContextResponse, error) { - rsp, err := c.GetAgentOAuthConsentContext(ctx, params, reqEditors...) - if err != nil { - return nil, err - } - return ParseGetAgentOAuthConsentContextResponse(rsp) -} - -// SubmitAgentOAuthConsentWithBodyWithResponse request with arbitrary body returning *SubmitAgentOAuthConsentResponse -func (c *ClientWithResponses) SubmitAgentOAuthConsentWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SubmitAgentOAuthConsentResponse, error) { - rsp, err := c.SubmitAgentOAuthConsentWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseSubmitAgentOAuthConsentResponse(rsp) -} - -func (c *ClientWithResponses) SubmitAgentOAuthConsentWithResponse(ctx context.Context, body SubmitAgentOAuthConsentJSONRequestBody, reqEditors ...RequestEditorFn) (*SubmitAgentOAuthConsentResponse, error) { - rsp, err := c.SubmitAgentOAuthConsent(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseSubmitAgentOAuthConsentResponse(rsp) -} - -// ListAgentOAuthGrantsWithResponse request returning *ListAgentOAuthGrantsResponse -func (c *ClientWithResponses) ListAgentOAuthGrantsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListAgentOAuthGrantsResponse, error) { - rsp, err := c.ListAgentOAuthGrants(ctx, reqEditors...) - if err != nil { - return nil, err - } - return ParseListAgentOAuthGrantsResponse(rsp) -} - -// RevokeAgentOAuthGrantWithResponse request returning *RevokeAgentOAuthGrantResponse -func (c *ClientWithResponses) RevokeAgentOAuthGrantWithResponse(ctx context.Context, grantId string, reqEditors ...RequestEditorFn) (*RevokeAgentOAuthGrantResponse, error) { - rsp, err := c.RevokeAgentOAuthGrant(ctx, grantId, reqEditors...) - if err != nil { - return nil, err - } - return ParseRevokeAgentOAuthGrantResponse(rsp) -} - // GetApiAuthAccountInfoWithResponse request returning *GetApiAuthAccountInfoResponse func (c *ClientWithResponses) GetApiAuthAccountInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiAuthAccountInfoResponse, error) { rsp, err := c.GetApiAuthAccountInfo(ctx, reqEditors...) @@ -41763,6 +41719,50 @@ func (c *ClientWithResponses) MarkNotificationReadWithResponse(ctx context.Conte return ParseMarkNotificationReadResponse(rsp) } +// GetOAuthConsentContextWithResponse request returning *GetOAuthConsentContextResponse +func (c *ClientWithResponses) GetOAuthConsentContextWithResponse(ctx context.Context, params *GetOAuthConsentContextParams, reqEditors ...RequestEditorFn) (*GetOAuthConsentContextResponse, error) { + rsp, err := c.GetOAuthConsentContext(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetOAuthConsentContextResponse(rsp) +} + +// SubmitOAuthConsentWithBodyWithResponse request with arbitrary body returning *SubmitOAuthConsentResponse +func (c *ClientWithResponses) SubmitOAuthConsentWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SubmitOAuthConsentResponse, error) { + rsp, err := c.SubmitOAuthConsentWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseSubmitOAuthConsentResponse(rsp) +} + +func (c *ClientWithResponses) SubmitOAuthConsentWithResponse(ctx context.Context, body SubmitOAuthConsentJSONRequestBody, reqEditors ...RequestEditorFn) (*SubmitOAuthConsentResponse, error) { + rsp, err := c.SubmitOAuthConsent(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseSubmitOAuthConsentResponse(rsp) +} + +// ListOAuthGrantsWithResponse request returning *ListOAuthGrantsResponse +func (c *ClientWithResponses) ListOAuthGrantsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListOAuthGrantsResponse, error) { + rsp, err := c.ListOAuthGrants(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseListOAuthGrantsResponse(rsp) +} + +// RevokeOAuthGrantWithResponse request returning *RevokeOAuthGrantResponse +func (c *ClientWithResponses) RevokeOAuthGrantWithResponse(ctx context.Context, grantId string, reqEditors ...RequestEditorFn) (*RevokeOAuthGrantResponse, error) { + rsp, err := c.RevokeOAuthGrant(ctx, grantId, reqEditors...) + if err != nil { + return nil, err + } + return ParseRevokeOAuthGrantResponse(rsp) +} + // ListOAuthResourceScopesWithResponse request returning *ListOAuthResourceScopesResponse func (c *ClientWithResponses) ListOAuthResourceScopesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListOAuthResourceScopesResponse, error) { rsp, err := c.ListOAuthResourceScopes(ctx, reqEditors...) @@ -43144,168 +43144,6 @@ func (c *ClientWithResponses) GetUserProfileWithResponse(ctx context.Context, us return ParseGetUserProfileResponse(rsp) } -// ParseGetAgentOAuthConsentContextResponse parses an HTTP response from a GetAgentOAuthConsentContextWithResponse call -func ParseGetAgentOAuthConsentContextResponse(rsp *http.Response) (*GetAgentOAuthConsentContextResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &GetAgentOAuthConsentContextResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest struct { - ClientId string `json:"clientId"` - ClientName string `json:"clientName"` - GrantLifetime struct { - AccessTokenSeconds *int `json:"accessTokenSeconds,omitempty"` - RefreshTokenSeconds *int `json:"refreshTokenSeconds,omitempty"` - } `json:"grantLifetime"` - InstanceOrigin string `json:"instanceOrigin"` - RedirectUri string `json:"redirectUri"` - Scopes []GetAgentOAuthConsentContext200JSONResponseBodyScopes `json:"scopes"` - StandardScopes []string `json:"standardScopes"` - Workspace struct { - Id string `json:"id"` - Name *string `json:"name"` - } `json:"workspace"` - } - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON403 = &dest - - } - - return response, nil -} - -// ParseSubmitAgentOAuthConsentResponse parses an HTTP response from a SubmitAgentOAuthConsentWithResponse call -func ParseSubmitAgentOAuthConsentResponse(rsp *http.Response) (*SubmitAgentOAuthConsentResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &SubmitAgentOAuthConsentResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest struct { - Url string `json:"url"` - } - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON403 = &dest - - } - - return response, nil -} - -// ParseListAgentOAuthGrantsResponse parses an HTTP response from a ListAgentOAuthGrantsWithResponse call -func ParseListAgentOAuthGrantsResponse(rsp *http.Response) (*ListAgentOAuthGrantsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &ListAgentOAuthGrantsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest struct { - Items []struct { - ClientId string `json:"clientId"` - ClientName string `json:"clientName"` - CreatedAt string `json:"createdAt"` - Id string `json:"id"` - LastUsedAt *string `json:"lastUsedAt"` - OrgId string `json:"orgId"` - Scopes []ListAgentOAuthGrants200JSONResponseBodyItemsScopes `json:"scopes"` - Status ListAgentOAuthGrants200JSONResponseBodyItemsStatus `json:"status"` - UserId string `json:"userId"` - WorkspaceName *string `json:"workspaceName"` - } `json:"items"` - } - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParseRevokeAgentOAuthGrantResponse parses an HTTP response from a RevokeAgentOAuthGrantWithResponse call -func ParseRevokeAgentOAuthGrantResponse(rsp *http.Response) (*RevokeAgentOAuthGrantResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &RevokeAgentOAuthGrantResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON404 = &dest - - } - - return response, nil -} - // ParseGetApiAuthAccountInfoResponse parses an HTTP response from a GetApiAuthAccountInfoWithResponse call func ParseGetApiAuthAccountInfoResponse(rsp *http.Response) (*GetApiAuthAccountInfoResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -53875,6 +53713,168 @@ func ParseMarkNotificationReadResponse(rsp *http.Response) (*MarkNotificationRea return response, nil } +// ParseGetOAuthConsentContextResponse parses an HTTP response from a GetOAuthConsentContextWithResponse call +func ParseGetOAuthConsentContextResponse(rsp *http.Response) (*GetOAuthConsentContextResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetOAuthConsentContextResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + ClientId string `json:"clientId"` + ClientName string `json:"clientName"` + GrantLifetime struct { + AccessTokenSeconds *int `json:"accessTokenSeconds,omitempty"` + RefreshTokenSeconds *int `json:"refreshTokenSeconds,omitempty"` + } `json:"grantLifetime"` + InstanceOrigin string `json:"instanceOrigin"` + RedirectUri string `json:"redirectUri"` + Scopes []GetOAuthConsentContext200JSONResponseBodyScopes `json:"scopes"` + StandardScopes []string `json:"standardScopes"` + Workspace struct { + Id string `json:"id"` + Name *string `json:"name"` + } `json:"workspace"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + } + + return response, nil +} + +// ParseSubmitOAuthConsentResponse parses an HTTP response from a SubmitOAuthConsentWithResponse call +func ParseSubmitOAuthConsentResponse(rsp *http.Response) (*SubmitOAuthConsentResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &SubmitOAuthConsentResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + Url string `json:"url"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + } + + return response, nil +} + +// ParseListOAuthGrantsResponse parses an HTTP response from a ListOAuthGrantsWithResponse call +func ParseListOAuthGrantsResponse(rsp *http.Response) (*ListOAuthGrantsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListOAuthGrantsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + Items []struct { + ClientId string `json:"clientId"` + ClientName string `json:"clientName"` + CreatedAt string `json:"createdAt"` + Id string `json:"id"` + LastUsedAt *string `json:"lastUsedAt"` + OrgId string `json:"orgId"` + Scopes []ListOAuthGrants200JSONResponseBodyItemsScopes `json:"scopes"` + Status ListOAuthGrants200JSONResponseBodyItemsStatus `json:"status"` + UserId string `json:"userId"` + WorkspaceName *string `json:"workspaceName"` + } `json:"items"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseRevokeOAuthGrantResponse parses an HTTP response from a RevokeOAuthGrantWithResponse call +func ParseRevokeOAuthGrantResponse(rsp *http.Response) (*RevokeOAuthGrantResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &RevokeOAuthGrantResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + // ParseListOAuthResourceScopesResponse parses an HTTP response from a ListOAuthResourceScopesWithResponse call func ParseListOAuthResourceScopesResponse(rsp *http.Response) (*ListOAuthResourceScopesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/docs/design/agent-authentication.md b/docs/design/oauth-server.md similarity index 94% rename from docs/design/agent-authentication.md rename to docs/design/oauth-server.md index 625b2b3e..4605df91 100644 --- a/docs/design/agent-authentication.md +++ b/docs/design/oauth-server.md @@ -1,4 +1,4 @@ -# External Agent Access — Design +# External OAuth Apps — Design > Status: Implemented > Scope: dynamic OAuth clients, external resource authorization, DPoP, resource @@ -13,8 +13,7 @@ grant for a DPoP-bound ZPan resource token. ZPan does not ship or require: -- a fixed first-party Agent OAuth client; -- an Agent-specific API key; +- a fixed first-party OAuth client; - Restish credential profiles in OpenAPI; - a Restish upload plugin; - a ZPan-specific Agent skill. @@ -100,7 +99,7 @@ Each consent is bound to: The request cannot replace that workspace with a query or body field. Team membership and role checks still apply. Revoking a consent removes its access -tokens, revokes its refresh tokens, and deletes the consent. The Agent Access +tokens, revokes its refresh tokens, and deletes the consent. The OAuth Apps page lists the real client name and workspace for every current-user grant. ## External Resource Token Flow @@ -116,7 +115,7 @@ FlareAuth-style controllers use three credentials with separate purposes: The exchanged access token is a JWT containing the user, workspace, `zpan_actor`, delegated actor (`act`), audience, scopes, client ID, expiry, and JTI. API requests use `Authorization: DPoP` plus a proof bound to the method, -URL, access token, and Agent key. ZPan verifies issuer, audience, signature, +URL, access token, and DPoP key. ZPan verifies issuer, audience, signature, expiry, scopes, DPoP proof, and JTI revocation. Revoking an exchanged JWT stores its JTI until token expiry. The resource API @@ -125,7 +124,7 @@ assertions are intentionally not part of this path. ## Scope Model -Resource scopes use stable `:` names. The external Agent scope +Resource scopes use stable `:` names. The external OAuth scope catalog includes: | Scope | Authority | @@ -151,13 +150,13 @@ OAuth is a credential adapter, not a business-logic fork. Middleware resolves a protocol-neutral principal, bound workspace, scope set, and audit actor before calling the same file use cases used by other authenticated clients. -Upgrades that add Agent scopes do not mutate OAuth tables during authentication +Upgrades that add OAuth resource scopes do not mutate OAuth tables during authentication startup. Before deploying such an upgrade, operators run the idempotent scope backfill in dry-run mode and then apply it: ```sh -pnpm agent-oauth-scopes:backfill -- --d1 zpan-db --remote -pnpm agent-oauth-scopes:backfill -- --d1 zpan-db --remote --apply +pnpm oauth-scopes:backfill -- --d1 zpan-db --remote +pnpm oauth-scopes:backfill -- --d1 zpan-db --remote --apply ``` For Node/SQLite deployments, replace the D1 arguments with @@ -197,8 +196,8 @@ re-presigning re-enter ZPan authorization and workspace checks. ## Compatibility Boundary The legacy `zpan-cli` device flow remains limited to downloader registration. -Ordinary human-created API keys remain available for their existing product -uses, but there is no Agent API-key template or Agent key management UI. +API keys remain available for image hosting, WebDAV, and remote-download access. +They are separate credentials and are not used by OAuth applications. Future client-registration approval can be added around dynamically registered client records without changing resource discovery, consent, token exchange, diff --git a/docs/roadmap/v2.9.md b/docs/roadmap/v2.9.md index fdcaee2e..3c6324b7 100644 --- a/docs/roadmap/v2.9.md +++ b/docs/roadmap/v2.9.md @@ -1,4 +1,4 @@ -# v2.9 — External Agent Access +# v2.9 — External OAuth Apps ## Goal @@ -19,9 +19,9 @@ ZPan owns: - scope-aware file APIs, Arazzo workflows, and structured direct-upload instructions. -The external controller owns Agent identity, approval of Agent access, delegated -credential injection, and tool orchestration. ZPan does not ship a fixed Agent -client or an Agent API-key product. +The external controller owns Agent identity, approval of application access, delegated +credential injection, and tool orchestration. ZPan does not ship a fixed OAuth +client. ## Deliverables @@ -102,8 +102,7 @@ controller or client ID. ## Removed Compatibility Surfaces -- fixed, system-managed Agent OAuth client; -- Agent API-key template, endpoints, settings forms, and tests; +- fixed, system-managed OAuth client; - OpenAPI `x-cli-config` profiles; - `restish-zpan` command plugin and release artifact; - repository-hosted ZPan Agent skill. @@ -120,7 +119,7 @@ single-use downloader-registration bootstrap. 5. Tool-neutral OpenAPI authorization metadata. 6. Discoverable Arazzo upload workflows. 7. Self-describing single/multipart upload responses. -8. Remove fixed-client, Agent API-key, profile, plugin, and skill surfaces. +8. Remove fixed-client, profile, plugin, and skill surfaces. 9. Complete local gates and real FlareAuth acceptance. ## Acceptance Criteria diff --git a/e2e/agent-access.spec.ts b/e2e/oauth-apps.spec.ts similarity index 87% rename from e2e/agent-access.spec.ts rename to e2e/oauth-apps.spec.ts index 3710919f..1b1561d8 100644 --- a/e2e/agent-access.spec.ts +++ b/e2e/oauth-apps.spec.ts @@ -4,11 +4,11 @@ import { signUpAndGoToFiles } from './helpers' const oauthQuery = 'client_id=dynamic-client&redirect_uri=https%3A%2F%2Fbroker.example.com%2Fcallback&response_type=code&scope=openid%20offline_access%20objects%3Aread%20shares%3Acreate%20quota%3Aread' -test.describe('Agent Access OAuth UI', () => { +test.describe('OAuth Apps UI', () => { test('renders consent details and submits full approval @desktop', async ({ page }) => { await signUpAndGoToFiles(page) - await page.route('**/api/agent-oauth-consent?*', async (route) => { + await page.route('**/api/oauth-consent?*', async (route) => { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ @@ -23,7 +23,7 @@ test.describe('Agent Access OAuth UI', () => { }), }) }) - await page.route('**/api/agent-oauth-consent', async (route) => { + await page.route('**/api/oauth-consent', async (route) => { if (route.request().method() !== 'POST') return route.fallback() expect(route.request().method()).toBe('POST') const body = route.request().postDataJSON() as { accept: boolean; oauthQuery?: string; scope?: string } @@ -37,7 +37,7 @@ test.describe('Agent Access OAuth UI', () => { await route.fulfill({ contentType: 'text/html', body: '
Returned to FlareAuth
' }) }) - await page.goto(`/settings/agent-access?${oauthQuery}`) + await page.goto(`/settings/oauth-apps?${oauthQuery}`) await expect(page.getByRole('heading', { name: 'Authorize Application' })).toBeVisible() await expect(page.getByText('http://localhost:5185')).toBeVisible() @@ -55,7 +55,7 @@ test.describe('Agent Access OAuth UI', () => { await signUpAndGoToFiles(page) let revoked = false - await page.route('**/api/agent-oauth-grants', async (route) => { + await page.route('**/api/oauth-grants', async (route) => { if (route.request().method() !== 'GET') return route.fallback() await route.fulfill({ contentType: 'application/json', @@ -79,15 +79,15 @@ test.describe('Agent Access OAuth UI', () => { }), }) }) - await page.route('**/api/agent-oauth-grants/grant-e2e', async (route) => { + await page.route('**/api/oauth-grants/grant-e2e', async (route) => { expect(route.request().method()).toBe('DELETE') revoked = true await route.fulfill({ status: 204 }) }) - await page.goto('/settings/agent-access') + await page.goto('/settings/oauth-apps') - await expect(page.getByText('Delegated OAuth Grants')).toBeVisible() + await expect(page.getByText('Authorized OAuth Apps')).toBeVisible() await expect(page.getByRole('cell', { name: 'FlareAuth' })).toBeVisible() await expect(page.getByText('Shares: create shares')).toBeVisible() @@ -95,13 +95,13 @@ test.describe('Agent Access OAuth UI', () => { await revokeButtons.last().click() await expect(page.getByRole('dialog', { name: 'Revoke OAuth Grant' })).toBeVisible() await page.getByRole('dialog').getByRole('button', { name: 'Revoke' }).click() - await expect(page.getByText('No delegated OAuth grants yet')).toBeVisible() + await expect(page.getByText('No authorized OAuth apps yet')).toBeVisible() }) test('keeps consent and delegated grants usable on narrow screens @mobile', async ({ page }) => { await signUpAndGoToFiles(page) - await page.route('**/api/agent-oauth-consent?*', async (route) => { + await page.route('**/api/oauth-consent?*', async (route) => { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ @@ -116,7 +116,7 @@ test.describe('Agent Access OAuth UI', () => { }), }) }) - await page.route('**/api/agent-oauth-grants', async (route) => { + await page.route('**/api/oauth-grants', async (route) => { if (route.request().method() !== 'GET') return route.fallback() await route.fulfill({ contentType: 'application/json', @@ -139,7 +139,7 @@ test.describe('Agent Access OAuth UI', () => { }) }) - await page.goto(`/settings/agent-access?${oauthQuery}`) + await page.goto(`/settings/oauth-apps?${oauthQuery}`) await expect(page.getByRole('heading', { name: 'Authorize Application' })).toBeVisible() await expect(page.getByRole('button', { name: 'Approve Access' })).toBeVisible() await expect(page.getByText('Files: read objects')).toBeVisible() @@ -148,8 +148,8 @@ test.describe('Agent Access OAuth UI', () => { .poll(async () => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)) .toBe(true) - await page.goto('/settings/agent-access') - await expect(page.getByText('Delegated OAuth Grants')).toBeVisible() + await page.goto('/settings/oauth-apps') + await expect(page.getByText('Authorized OAuth Apps')).toBeVisible() await expect(page.getByRole('cell', { name: 'FlareAuth' })).toBeVisible() const grantsTableContainer = page.locator('[data-slot="table-container"]').last() await expect(grantsTableContainer).toBeVisible() diff --git a/package.json b/package.json index 93db9f07..82fc8ad5 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "storage:backfill": "tsx scripts/backfill-storage-usage.ts", "storage-status:backfill": "tsx scripts/backfill-storage-enabled-status.ts", "api-key-scopes:backfill": "tsx scripts/backfill-api-key-scopes.ts", - "agent-oauth-scopes:backfill": "tsx scripts/backfill-agent-oauth-scopes.ts", + "oauth-scopes:backfill": "tsx scripts/backfill-oauth-scopes.ts", "typecheck": "tsc --noEmit -p server/tsconfig.json && tsc --noEmit -p src/tsconfig.json", "test": "vitest run --project unit --project integration", "test:cf": "vitest run --project cloudflare", diff --git a/scripts/backfill-agent-oauth-scopes.ts b/scripts/backfill-oauth-scopes.ts similarity index 74% rename from scripts/backfill-agent-oauth-scopes.ts rename to scripts/backfill-oauth-scopes.ts index 585760c1..c4dc89eb 100644 --- a/scripts/backfill-agent-oauth-scopes.ts +++ b/scripts/backfill-oauth-scopes.ts @@ -2,16 +2,16 @@ import { execFileSync } from 'node:child_process' import Database from 'better-sqlite3' -import { AGENT_OAUTH_SCOPES } from '../shared/agent-oauth' +import { OAUTH_SCOPES } from '../shared/oauth' import { AuthorizationScope } from '../shared/authorization' -export type AgentOAuthScopeBackfillTarget = +export type OAuthScopeBackfillTarget = | { kind: 'sqlite'; path: string } | { kind: 'd1'; database: string; remote: boolean; env?: string } -export interface AgentOAuthScopeBackfillOptions { +export interface OAuthScopeBackfillOptions { apply: boolean - target: AgentOAuthScopeBackfillTarget + target: OAuthScopeBackfillTarget } interface OAuthResourceRow { @@ -25,16 +25,16 @@ interface OAuthClientRow { scopes: string | null } -export interface AgentOAuthScopeBackfill { +export interface OAuthScopeBackfill { resources: Array<{ id: string; scopes: string }> clients: Array<{ id: string; scopes: string }> } -export function buildAgentOAuthScopeBackfill( +export function buildOAuthScopeBackfill( resources: OAuthResourceRow[], clients: OAuthClientRow[], -): AgentOAuthScopeBackfill { - const resourceScopes = JSON.stringify(AGENT_OAUTH_SCOPES) +): OAuthScopeBackfill { + const resourceScopes = JSON.stringify(OAUTH_SCOPES) return { resources: resources.flatMap((resource) => resource.name === 'ZPan API' && resource.allowedScopes !== resourceScopes @@ -60,7 +60,7 @@ function parseScopes(value: string | null): string[] { return parsed } -export function parseAgentOAuthScopeBackfillOptions(argv: string[]): AgentOAuthScopeBackfillOptions { +export function parseOAuthScopeBackfillOptions(argv: string[]): OAuthScopeBackfillOptions { const sqliteIndex = argv.indexOf('--sqlite') const d1Index = argv.indexOf('--d1') if ((sqliteIndex >= 0) === (d1Index >= 0)) usage() @@ -85,11 +85,11 @@ export function parseAgentOAuthScopeBackfillOptions(argv: string[]): AgentOAuthS function usage(): never { throw new Error( - 'Usage: pnpm agent-oauth-scopes:backfill -- (--sqlite | --d1 [--remote] [--env ]) [--apply]', + 'Usage: pnpm oauth-scopes:backfill -- (--sqlite | --d1 [--remote] [--env ]) [--apply]', ) } -function d1Args(target: Extract): string[] { +function d1Args(target: Extract): string[] { return [ 'exec', 'wrangler', @@ -101,27 +101,27 @@ function d1Args(target: Extract): ] } -function executeD1(target: Extract, sql: string, json = false): string { +function executeD1(target: Extract, sql: string, json = false): string { return execFileSync('pnpm', [...d1Args(target), '--command', sql, ...(json ? ['--json'] : [])], { encoding: 'utf8', stdio: json ? 'pipe' : 'inherit', }) as string } -export type AgentOAuthScopeD1Executor = typeof executeD1 +export type OAuthScopeD1Executor = typeof executeD1 function d1Rows( - target: Extract, + target: Extract, sql: string, - execute: AgentOAuthScopeD1Executor, + execute: OAuthScopeD1Executor, ): T[] { const payload = JSON.parse(execute(target, sql, true)) as Array<{ results?: T[] }> return payload.flatMap((entry) => entry.results ?? []) } function readRows( - target: AgentOAuthScopeBackfillTarget, - execute: AgentOAuthScopeD1Executor, + target: OAuthScopeBackfillTarget, + execute: OAuthScopeD1Executor, ): { resources: OAuthResourceRow[]; clients: OAuthClientRow[] } { const resourceSql = 'SELECT id, name, allowed_scopes AS allowedScopes FROM oauthResource;' const clientSql = 'SELECT id, scopes FROM oauthClient;' @@ -147,9 +147,9 @@ function sqlString(value: string): string { } function applyBackfill( - target: AgentOAuthScopeBackfillTarget, - changes: AgentOAuthScopeBackfill, - execute: AgentOAuthScopeD1Executor, + target: OAuthScopeBackfillTarget, + changes: OAuthScopeBackfill, + execute: OAuthScopeD1Executor, ): void { if (target.kind === 'd1') { for (const resource of changes.resources) { @@ -180,18 +180,18 @@ function applyBackfill( } } -function countChanges(changes: AgentOAuthScopeBackfill): number { +function countChanges(changes: OAuthScopeBackfill): number { return changes.resources.length + changes.clients.length } -export function runAgentOAuthScopeBackfill( +export function runOAuthScopeBackfill( argv: string[], log: (message: string) => void = console.log, - execute: AgentOAuthScopeD1Executor = executeD1, + execute: OAuthScopeD1Executor = executeD1, ): void { - const options = parseAgentOAuthScopeBackfillOptions(argv) + const options = parseOAuthScopeBackfillOptions(argv) const rows = readRows(options.target, execute) - const changes = buildAgentOAuthScopeBackfill(rows.resources, rows.clients) + const changes = buildOAuthScopeBackfill(rows.resources, rows.clients) log( JSON.stringify( { @@ -206,8 +206,8 @@ export function runAgentOAuthScopeBackfill( if (!options.apply) return applyBackfill(options.target, changes, execute) const after = readRows(options.target, execute) - const remaining = buildAgentOAuthScopeBackfill(after.resources, after.clients) - if (countChanges(remaining) > 0) throw new Error(`agent_oauth_scope_backfill_failed:${countChanges(remaining)}`) + const remaining = buildOAuthScopeBackfill(after.resources, after.clients) + if (countChanges(remaining) > 0) throw new Error(`oauth_scope_backfill_failed:${countChanges(remaining)}`) } -if (process.argv[1]?.endsWith('backfill-agent-oauth-scopes.ts')) runAgentOAuthScopeBackfill(process.argv.slice(2)) +if (process.argv[1]?.endsWith('backfill-oauth-scopes.ts')) runOAuthScopeBackfill(process.argv.slice(2)) diff --git a/server/adapters/repos/audit.ts b/server/adapters/repos/audit.ts index cd630586..ca432c9c 100644 --- a/server/adapters/repos/audit.ts +++ b/server/adapters/repos/audit.ts @@ -58,9 +58,11 @@ export function idempotentSystemEventValues(input: { } function normalizeActorType(value: string | null, userId?: string | null): AuditActorType { + // Audit rows written before OAuth was generalized used the old compound actor type. + if (value === ['agent', 'oauth'].join('_')) return 'oauth' if ( value === 'api_key' || - value === 'agent_oauth' || + value === 'oauth' || value === 'agent' || value === 'anonymous' || value === 'system' || @@ -75,7 +77,7 @@ function normalizeActorType(value: string | null, userId?: string | null): Audit function actorDisplayName(actorType: AuditActorType, actorRef: string | null): string { if (actorType === 'anonymous') return 'Anonymous' if (actorType === 'api_key') return actorRef ? `API key:${actorRef}` : 'API key' - if (actorType === 'agent_oauth') return actorRef ? `Agent OAuth:${actorRef}` : 'Agent OAuth' + if (actorType === 'oauth') return actorRef ? `OAuth:${actorRef}` : 'OAuth' if (actorType === 'agent') return actorRef ? `Agent:${actorRef}` : 'Agent' if (actorType === 'system') return actorRef ? `System:${actorRef}` : 'System' if (actorType === 'downloader') return actorRef ? `Downloader:${actorRef}` : 'Downloader' diff --git a/server/adapters/repos/agent-oauth.test.ts b/server/adapters/repos/oauth.test.ts similarity index 90% rename from server/adapters/repos/agent-oauth.test.ts rename to server/adapters/repos/oauth.test.ts index e8deb9c7..f9f4d9c0 100644 --- a/server/adapters/repos/agent-oauth.test.ts +++ b/server/adapters/repos/oauth.test.ts @@ -3,27 +3,27 @@ import { isNull } from 'drizzle-orm' import { describe, expect, it } from 'vitest' import * as authSchema from '../../db/auth-schema' import { createTestApp } from '../../test/setup' -import { createAgentOAuthGateway } from './agent-oauth' +import { createOAuthGateway } from './oauth' const CLIENT_ID = 'dynamic-client' -describe('Agent OAuth gateway', () => { +describe('OAuth gateway', () => { it('finds and lists dynamically registered applications', async () => { const { db } = await createTestApp() await insertClient(db, CLIENT_ID, 'FlareAuth') await insertClient(db, 'retired-system-client', 'Retired', 'system') - await expect(createAgentOAuthGateway().findClient(db, CLIENT_ID)).resolves.toMatchObject({ + await expect(createOAuthGateway().findClient(db, CLIENT_ID)).resolves.toMatchObject({ clientId: CLIENT_ID, clientName: 'FlareAuth', disabled: false, redirectUris: ['https://flareauth.example/callback'], responseTypes: ['code'], }) - await expect(createAgentOAuthGateway().listRegisteredApplications(db)).resolves.toEqual([ + await expect(createOAuthGateway().listRegisteredApplications(db)).resolves.toEqual([ expect.objectContaining({ clientId: CLIENT_ID, name: 'FlareAuth' }), ]) - await expect(createAgentOAuthGateway().findClient(db, 'retired-system-client')).resolves.toBeNull() + await expect(createOAuthGateway().findClient(db, 'retired-system-client')).resolves.toBeNull() }) it('lists workspace-bound grants with their registered application names', async () => { @@ -54,7 +54,7 @@ describe('Agent OAuth gateway', () => { }, ]) - await expect(createAgentOAuthGateway().listGrants(db, userId)).resolves.toEqual([ + await expect(createOAuthGateway().listGrants(db, userId)).resolves.toEqual([ { id: 'grant-1', clientId: CLIENT_ID, @@ -106,7 +106,7 @@ describe('Agent OAuth gateway', () => { }) await expect( - createAgentOAuthGateway().revokeGrant(db, { + createOAuthGateway().revokeGrant(db, { userId, grantId: 'grant-1', now: new Date('2026-07-29T12:30:00.000Z'), @@ -127,7 +127,7 @@ describe('Agent OAuth gateway', () => { JSON.stringify({ jti: 'token-1', client_id: CLIENT_ID, exp: Math.floor(Date.now() / 1000) + 60 }), ).toString('base64url') const token = `e30.${payload}.signature` - const gateway = createAgentOAuthGateway() + const gateway = createOAuthGateway() await gateway.revokeJwtAccessToken(db, token) diff --git a/server/adapters/repos/agent-oauth.ts b/server/adapters/repos/oauth.ts similarity index 97% rename from server/adapters/repos/agent-oauth.ts rename to server/adapters/repos/oauth.ts index 25f27f07..36768a98 100644 --- a/server/adapters/repos/agent-oauth.ts +++ b/server/adapters/repos/oauth.ts @@ -9,9 +9,9 @@ import { oauthRefreshToken, } from '../../db/auth-schema' import { executeWriteTransaction } from '../../db/transaction' -import type { AgentOAuthClient, AgentOAuthGateway } from '../../usecases/ports' +import type { OAuthClient, OAuthGateway } from '../../usecases/ports' -export function createAgentOAuthGateway(): AgentOAuthGateway { +export function createOAuthGateway(): OAuthGateway { return { async findClient(db, clientId) { const [row] = await db @@ -35,7 +35,7 @@ export function createAgentOAuthGateway(): AgentOAuthGateway { redirectUris: parseStringArray(row.redirectUris), responseTypes: parseStringArray(row.responseTypes), scopes: parseStringArray(row.scopes), - } satisfies AgentOAuthClient + } satisfies OAuthClient }, async listRegisteredApplications(db) { diff --git a/server/adapters/repos/org.test.ts b/server/adapters/repos/org.test.ts index 3fcd7df5..5bd1eddd 100644 --- a/server/adapters/repos/org.test.ts +++ b/server/adapters/repos/org.test.ts @@ -159,19 +159,3 @@ describe('isPersonalOrg', () => { expect(result).toBe(false) }) }) - -describe('canManageAgentAccess', () => { - it.each([ - ['owner', true], - ['admin', true], - ['editor', false], - ['viewer', false], - ])('allows Agent Access management for %s: %s', async (role, expected) => { - const { db } = await createTestApp() - const userId = await insertUser(db) - const orgId = await insertOrg(db, { metadata: '{"type":"team"}' }) - await insertMember(db, orgId, userId, role) - - expect(await createOrgRepo(db).canManageAgentAccess(userId, orgId)).toBe(expected) - }) -}) diff --git a/server/adapters/repos/org.ts b/server/adapters/repos/org.ts index adeb2268..b91254fe 100644 --- a/server/adapters/repos/org.ts +++ b/server/adapters/repos/org.ts @@ -65,11 +65,5 @@ export function createOrgRepo(db: Database): OrgRepo { return orgId === (await findPersonalOrg(userId)) } - async function canManageAgentAccess(userId: string, orgId: string): Promise { - const role = await getMemberRole(orgId, userId) - if (role !== null) return role === 'owner' || role === 'admin' - return orgId === (await findPersonalOrg(userId)) - } - - return { findPersonalOrg, getMemberRole, getOrgNames, canReadOrg, canWriteToOrg, canManageAgentAccess, isPersonalOrg } + return { findPersonalOrg, getMemberRole, getOrgNames, canReadOrg, canWriteToOrg, isPersonalOrg } } diff --git a/server/app.ts b/server/app.ts index d6485c8a..ae950a2c 100644 --- a/server/app.ts +++ b/server/app.ts @@ -1,7 +1,7 @@ import { release as osRelease } from 'node:os' import { OpenAPIHono } from '@hono/zod-openapi' import { Scalar } from '@scalar/hono-api-reference' -import { AGENT_OAUTH_SCOPE_DESCRIPTIONS, AGENT_OAUTH_SCOPES } from '@shared/agent-oauth' +import { OAUTH_SCOPE_DESCRIPTIONS, OAUTH_SCOPES } from '@shared/oauth' import type { Context } from 'hono' import { cors } from 'hono/cors' import type { Auth } from './auth' @@ -10,7 +10,6 @@ import { createDeps } from './composition' import { isPotentialWebDavPublicRequest, isWebDavPublicRequest } from './domain/webdav-public-url' import { adminOverview } from './http/admin-overview' import { adminStats } from './http/admin-stats' -import { agentOAuthGrants } from './http/agent-oauth-grants' import { ARAZZO_DOCUMENT_PATH, ARAZZO_MEDIA_TYPE, createArazzoDocument } from './http/arazzo' import { serveAvatarBlob } from './http/avatar-blobs' import backgroundJobs from './http/background-jobs' @@ -22,6 +21,7 @@ import ihostConfig from './http/image-hosting/config' import ihost from './http/image-hosting/images' import internal from './http/internal' import { notifications } from './http/notifications' +import { oauthGrants } from './http/oauth-grants' import { oauthResourceScopes } from './http/oauth-resource-scopes' import objects from './http/objects' import { adminQuotas, userQuotas } from './http/quotas' @@ -146,7 +146,7 @@ export function createApp(platform: Platform, auth: Auth, deps: Deps = createDep if (error?.error === 'unsupported_token_type') { const token = (await revokeRequest.formData()).get('token') if (typeof token === 'string') { - await c.get('deps').agentOAuth.revokeJwtAccessToken(c.get('platform').db, token) + await c.get('deps').oauth.revokeJwtAccessToken(c.get('platform').db, token) return new Response(null, { status: 200, headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' }, @@ -172,7 +172,7 @@ export function createApp(platform: Platform, auth: Auth, deps: Deps = createDep resource: `${origin}/api`, authorization_servers: [authorizationServer], bearer_methods_supported: ['header'], - scopes_supported: AGENT_OAUTH_SCOPES.filter((scope) => scope.includes(':')), + scopes_supported: OAUTH_SCOPES.filter((scope) => scope.includes(':')), dpop_signing_alg_values_supported: ['ES256', 'EdDSA'], resource_name: 'ZPan API', }) @@ -237,7 +237,7 @@ export function createApp(platform: Platform, auth: Auth, deps: Deps = createDep ...(doc.components.securitySchemes ?? {}), cookieAuth: { type: 'apiKey', in: 'cookie', name: 'zp.session_token' }, bearerAuth: { type: 'http', scheme: 'bearer' }, - agentOAuth2: { + oauth2: { type: 'oauth2', flows: { authorizationCode: { @@ -357,7 +357,7 @@ export function createApp(platform: Platform, auth: Auth, deps: Deps = createDep app.route('/api/objects', objects) app.route('/api/shares', authedShares) app.route('/api/trash', trash) - app.route('/api', agentOAuthGrants) + app.route('/api', oauthGrants) app.route('/api/teams', teams) app.route('/api/teams', adminTeams) app.route('/api/site/storages', storages) @@ -462,7 +462,7 @@ function getCorsOrigins(platform: Platform): Set { } function agentScopeDescriptions(): Record { - return { ...AGENT_OAUTH_SCOPE_DESCRIPTIONS } + return { ...OAUTH_SCOPE_DESCRIPTIONS } } export type AppType = ReturnType @@ -508,5 +508,5 @@ export type AdminAuditRoute = typeof adminAudit export type AdminOverviewRoute = typeof adminOverview export type AdminStatsRoute = typeof adminStats export type StorageUsageRoute = typeof storageUsage -export type AgentOAuthGrantsRoute = typeof agentOAuthGrants +export type OAuthGrantsRoute = typeof oauthGrants export type OAuthResourceScopesRoute = typeof oauthResourceScopes diff --git a/server/auth.integration.test.ts b/server/auth.integration.test.ts index 87df61b7..152ac5e4 100644 --- a/server/auth.integration.test.ts +++ b/server/auth.integration.test.ts @@ -795,7 +795,7 @@ describe('Cloudflare Workers preview auth origins', () => { }) }) -describe('Agent OAuth consent guards', () => { +describe('OAuth consent guards', () => { it('publishes the external resource discovery contract at the exact API URL', async () => { const ctx = await createTestApp() const resource = await ctx.app.request('http://localhost:3000/api') @@ -927,7 +927,7 @@ describe('Agent OAuth consent guards', () => { const ctx = await createTestApp() ctx.app.get('/api/test-agent-audit', async (c) => { const principal = c.get('principal') - if (principal?.kind !== 'agent-oauth') return c.json({ error: 'agent principal required' }, 401) + if (principal?.kind !== 'oauth') return c.json({ error: 'agent principal required' }, 401) await c.get('deps').audit.record({ ...auditActor(principal), orgId: principal.orgId, @@ -937,15 +937,15 @@ describe('Agent OAuth consent guards', () => { }) return c.json({ ok: true }) }) - const { privateKey: agentPrivateKey, publicKey: agentPublicKey } = await generateKeyPair('ES256') - const agentPublicJwk = { ...(await exportJWK(agentPublicKey)), kid: 'agent-key', use: 'sig', alg: 'ES256' } + const { privateKey: actorPrivateKey, publicKey: actorPublicKey } = await generateKeyPair('ES256') + const actorPublicJwk = { ...(await exportJWK(actorPublicKey)), kid: 'actor-key', use: 'sig', alg: 'ES256' } const getJwks = ctx.auth.api.getJwks vi.stubGlobal( 'fetch', vi.fn(async (input: string | URL | Request) => { const url = input instanceof Request ? input.url : String(input) if (url === 'https://broker.example.com/api/auth/jwks') { - return Response.json({ keys: [agentPublicJwk] }) + return Response.json({ keys: [actorPublicJwk] }) } if (url === 'http://localhost:3000/api/auth/jwks') { return Response.json(await getJwks()) @@ -1000,7 +1000,7 @@ describe('Agent OAuth consent guards', () => { ) const consentLocation = authorize.headers.get('location') expect(authorize.status).toBe(302) - expect(consentLocation).toMatch(/^\/settings\/agent-access\?/) + expect(consentLocation).toMatch(/^\/settings\/oauth-apps\?/) const consent = await ctx.app.request('http://localhost:3000/api/auth/oauth2/consent', { method: 'POST', headers: { Cookie: cookie, Origin: 'http://localhost:3000', 'Content-Type': 'application/json' }, @@ -1032,14 +1032,14 @@ describe('Agent OAuth consent guards', () => { const now = Math.floor(Date.now() / 1000) const assertion = await new SignJWT({}) - .setProtectedHeader({ typ: 'JWT', alg: 'ES256', kid: 'agent-key' }) + .setProtectedHeader({ typ: 'JWT', alg: 'ES256', kid: 'actor-key' }) .setIssuer('https://broker.example.com/api/auth') .setSubject('agent-123') .setAudience(tokenEndpoint) .setIssuedAt(now) .setExpirationTime(now + 300) .setJti(crypto.randomUUID()) - .sign(agentPrivateKey) + .sign(actorPrivateKey) const actorResponse = await ctx.app.request(tokenEndpoint, { method: 'POST', headers: { Authorization: basic, 'Content-Type': 'application/x-www-form-urlencoded' }, @@ -1102,7 +1102,7 @@ describe('Agent OAuth consent guards', () => { .from(schema.auditEvents) .where(eq(schema.auditEvents.action, 'agent_identity_probe')) expect(auditEvent).toMatchObject({ - actorType: 'agent_oauth', + actorType: 'oauth', actorRef: 'agent-123', actorIssuer: 'https://broker.example.com/api/auth', }) @@ -1138,7 +1138,7 @@ describe('Agent OAuth consent guards', () => { const previewOrigin = 'https://preview-zpan.example.com' const auth = await createAuth(ctx.platform, 'test-secret', 'https://zpan-staging.example.com', [previewOrigin]) const app = createApp(ctx.platform, auth) - const signUpResponse = await signUp({ ...ctx, app }, 'agent-oauth-consent@example.com') + const signUpResponse = await signUp({ ...ctx, app }, 'oauth-consent@example.com') const cookie = signUpResponse.headers .getSetCookie() .map((value) => value.split(';', 1)[0]) @@ -1171,7 +1171,7 @@ describe('Agent OAuth consent guards', () => { }) const consentLocation = authorize.headers.get('location') expect(authorize.status).toBe(302) - expect(consentLocation).toMatch(/^\/settings\/agent-access\?/) + expect(consentLocation).toMatch(/^\/settings\/oauth-apps\?/) const consent = await app.request(`${previewOrigin}/api/auth/oauth2/consent`, { method: 'POST', @@ -1193,7 +1193,7 @@ describe('Agent OAuth consent guards', () => { }) }) - it('blocks partial Agent OAuth consent changes through the Better Auth endpoint', async () => { + it('blocks partial OAuth consent changes through the Better Auth endpoint', async () => { const ctx = await createTestApp() const res = await ctx.app.request('/api/auth/oauth2/consent', { @@ -1205,7 +1205,7 @@ describe('Agent OAuth consent guards', () => { expect(res.status).toBe(400) await expect(res.json()).resolves.toMatchObject({ error: 'invalid_request', - error_description: 'Partial Agent OAuth consent is not supported', + error_description: 'Partial OAuth consent is not supported', }) }) }) diff --git a/server/auth.ts b/server/auth.ts index 2fdf1dfb..7f3c76c4 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -19,7 +19,6 @@ import { genericOAuth } from 'better-auth/plugins/generic-oauth' import { adminAc, memberAc, ownerAc } from 'better-auth/plugins/organization/access' import { count, eq, like } from 'drizzle-orm' import { customAlphabet, nanoid } from 'nanoid' -import { AGENT_OAUTH_SCOPES, JWT_BEARER_GRANT_TYPE, TOKEN_EXCHANGE_GRANT_TYPE } from '../shared/agent-oauth' import { API_KEY_TEMPLATES, ApiKeyTemplate, @@ -32,6 +31,7 @@ import { WEBDAV_RATE_LIMITER_BINDING, } from '../shared/api-key-templates' import { DEFAULT_ORG_QUOTA, DEFAULT_ORG_TRAFFIC_QUOTA, SignupMode } from '../shared/constants' +import { JWT_BEARER_GRANT_TYPE, OAUTH_SCOPES, TOKEN_EXCHANGE_GRANT_TYPE } from '../shared/oauth' import { BUILTIN_PROVIDER_IDS, OAUTH_PROVIDER_KEY_PATTERN, @@ -54,7 +54,7 @@ import { createSiteInvitationRepo } from './adapters/repos/site-invitations' import { initialStorageUsageProjectionQueries } from './adapters/repos/storage-usage-breakdown' import { createSystemOptionsRepo } from './adapters/repos/system-options' import { recordUserActivity } from './adapters/repos/user-activity' -import { createAgentOAuthProviderOptions } from './auth/agent-oauth-provider' +import { createOAuthProviderOptions } from './auth/oauth-provider' import * as authSchema from './db/auth-schema' import { orgQuotaEntitlements, orgQuotas, systemOptions } from './db/schema' import { executeWriteTransaction } from './db/transaction' @@ -520,7 +520,7 @@ export async function createAuth( if (body?.scope !== undefined) { throw new APIError('BAD_REQUEST', { error: 'invalid_request', - error_description: 'Partial Agent OAuth consent is not supported', + error_description: 'Partial OAuth consent is not supported', }) } return @@ -528,14 +528,14 @@ export async function createAuth( if (ctx.path === '/oauth2/register') { const body = ctx.body as Record | undefined if (body && isExternalResourceClientRegistration(body)) { - body.scope = AGENT_OAUTH_SCOPES.join(' ') + body.scope = OAUTH_SCOPES.join(' ') } return } if (ctx.path === '/oauth2/update-consent' || ctx.path === '/oauth2/delete-consent') { throw new APIError('FORBIDDEN', { error: 'invalid_request', - error_description: 'Manage Agent OAuth grants from the Agent Access API', + error_description: 'Manage OAuth grants from the OAuth grants API', }) } if (ctx.path !== '/api-key/create') return @@ -721,7 +721,7 @@ export async function createAuth( validateClient: async (clientId) => clientId === LEGACY_DOWNLOADER_CLIENT_ID, }), jwt(), - oauthProvider(createAgentOAuthProviderOptions({ db, resourceAudience })), + oauthProvider(createOAuthProviderOptions({ db, resourceAudience })), apiKey([ { configId: ApiKeyTemplate.IHOST, diff --git a/server/auth/agent-oauth-provider.test.ts b/server/auth/oauth-provider.test.ts similarity index 85% rename from server/auth/agent-oauth-provider.test.ts rename to server/auth/oauth-provider.test.ts index 05fbf44b..fbe6a793 100644 --- a/server/auth/agent-oauth-provider.test.ts +++ b/server/auth/oauth-provider.test.ts @@ -1,7 +1,7 @@ -import { AGENT_OAUTH_ACCESS_TOKEN_SECONDS, AGENT_OAUTH_SCOPES } from '@shared/agent-oauth' import { AuthorizationScope } from '@shared/authorization' +import { OAUTH_ACCESS_TOKEN_SECONDS, OAUTH_SCOPES } from '@shared/oauth' import { describe, expect, it, vi } from 'vitest' -import { createAgentOAuthProviderOptions } from './agent-oauth-provider' +import { createOAuthProviderOptions } from './oauth-provider' const db = {} as never @@ -9,7 +9,7 @@ function createOptions(input?: { findPersonalOrg?: (userId: string) => Promise getMemberRole?: (orgId: string, userId: string) => Promise }) { - return createAgentOAuthProviderOptions({ + return createOAuthProviderOptions({ db, orgs: { findPersonalOrg: input?.findPersonalOrg ?? vi.fn(async () => 'personal-org'), @@ -18,18 +18,18 @@ function createOptions(input?: { }) } -describe('createAgentOAuthProviderOptions', () => { +describe('createOAuthProviderOptions', () => { it('configures a dynamic-client OAuth provider contract', async () => { const options = createOptions() expect(options).toMatchObject({ loginPage: '/sign-in', - consentPage: '/settings/agent-access', - accessTokenExpiresIn: AGENT_OAUTH_ACCESS_TOKEN_SECONDS, + consentPage: '/settings/oauth-apps', + accessTokenExpiresIn: OAUTH_ACCESS_TOKEN_SECONDS, grantTypes: ['authorization_code', 'refresh_token'], - postLogin: { page: '/settings/agent-access' }, + postLogin: { page: '/settings/oauth-apps' }, }) - expect(options.scopes).toEqual([...AGENT_OAUTH_SCOPES]) + expect(options.scopes).toEqual([...OAUTH_SCOPES]) expect(options.cachedTrustedClients).toBeUndefined() expect(options.allowDynamicClientRegistration).toBe(true) expect(options.allowUnauthenticatedClientRegistration).toBe(true) @@ -79,7 +79,7 @@ describe('createAgentOAuthProviderOptions', () => { scopes: [AuthorizationScope.OBJECTS_READ], } as never), ).rejects.toMatchObject({ - body: expect.objectContaining({ error_description: 'A workspace is required for Agent OAuth' }), + body: expect.objectContaining({ error_description: 'A workspace is required for OAuth' }), }) await expect( @@ -92,7 +92,7 @@ describe('createAgentOAuthProviderOptions', () => { scopes: [AuthorizationScope.OBJECTS_READ], } as never), ).rejects.toMatchObject({ - body: expect.objectContaining({ error_description: 'Workspace access is required for Agent OAuth' }), + body: expect.objectContaining({ error_description: 'Workspace access is required for OAuth' }), }) }) @@ -103,7 +103,7 @@ describe('createAgentOAuthProviderOptions', () => { user: { id: 'user-1' }, referenceId: 'team-org', } as never), - ).resolves.toEqual({ zpan_org_id: 'team-org', zpan_actor: 'agent_oauth' }) + ).resolves.toEqual({ zpan_org_id: 'team-org', zpan_actor: 'oauth' }) }) it('omits ZPan resource claims without user or workspace context', async () => { diff --git a/server/auth/agent-oauth-provider.ts b/server/auth/oauth-provider.ts similarity index 86% rename from server/auth/agent-oauth-provider.ts rename to server/auth/oauth-provider.ts index 07c6254f..3b39d31d 100644 --- a/server/auth/agent-oauth-provider.ts +++ b/server/auth/oauth-provider.ts @@ -7,42 +7,42 @@ import { } from '@better-auth/oauth-provider' import { APIError, type User } from 'better-auth' import { createLocalJWKSet, createRemoteJWKSet, type JSONWebKeySet, jwtVerify } from 'jose' +import { isAuthorizationScope } from '../../shared/authorization' import { AGENT_ACTOR_RESOURCE, - AGENT_OAUTH_ACCESS_TOKEN_SECONDS, - AGENT_OAUTH_ACTOR_TOKEN_SECONDS, - AGENT_OAUTH_REFRESH_TOKEN_SECONDS, - AGENT_OAUTH_SCOPES, - AGENT_OAUTH_STANDARD_SCOPES, JWT_BEARER_GRANT_TYPE, + OAUTH_ACCESS_TOKEN_SECONDS, OAUTH_ACCESS_TOKEN_TYPE, + OAUTH_ACTOR_TOKEN_SECONDS, + OAUTH_REFRESH_TOKEN_SECONDS, + OAUTH_SCOPES, + OAUTH_STANDARD_SCOPES, TOKEN_EXCHANGE_GRANT_TYPE, -} from '../../shared/agent-oauth' -import { isAuthorizationScope } from '../../shared/authorization' +} from '../../shared/oauth' import { createOrgRepo } from '../adapters/repos/org' import type { Database } from '../platform/interface' -type AgentOAuthOrgLookup = Pick, 'findPersonalOrg' | 'getMemberRole'> -type AgentOAuthProviderOptions = Parameters[0] +type OAuthOrgLookup = Pick, 'findPersonalOrg' | 'getMemberRole'> +type OAuthProviderOptions = Parameters[0] -export function createAgentOAuthProviderOptions(input: { +export function createOAuthProviderOptions(input: { db: Database resourceAudience?: string - orgs?: AgentOAuthOrgLookup -}): AgentOAuthProviderOptions { + orgs?: OAuthOrgLookup +}): OAuthProviderOptions { const orgs = input.orgs ?? createOrgRepo(input.db) const resources = input.resourceAudience ? [ { identifier: input.resourceAudience, name: 'ZPan API', - accessTokenTtl: AGENT_OAUTH_ACCESS_TOKEN_SECONDS, - allowedScopes: [...AGENT_OAUTH_SCOPES], + accessTokenTtl: OAUTH_ACCESS_TOKEN_SECONDS, + allowedScopes: [...OAUTH_SCOPES], }, { identifier: AGENT_ACTOR_RESOURCE, name: 'ZPan Agent Actor', - accessTokenTtl: AGENT_OAUTH_ACTOR_TOKEN_SECONDS, + accessTokenTtl: OAUTH_ACTOR_TOKEN_SECONDS, allowedScopes: ['openid'], }, ] @@ -50,42 +50,42 @@ export function createAgentOAuthProviderOptions(input: { return { loginPage: '/sign-in', - consentPage: '/settings/agent-access', - accessTokenExpiresIn: AGENT_OAUTH_ACCESS_TOKEN_SECONDS, - m2mAccessTokenExpiresIn: AGENT_OAUTH_ACTOR_TOKEN_SECONDS, - refreshTokenExpiresIn: AGENT_OAUTH_REFRESH_TOKEN_SECONDS, + consentPage: '/settings/oauth-apps', + accessTokenExpiresIn: OAUTH_ACCESS_TOKEN_SECONDS, + m2mAccessTokenExpiresIn: OAUTH_ACTOR_TOKEN_SECONDS, + refreshTokenExpiresIn: OAUTH_REFRESH_TOKEN_SECONDS, grantTypes: ['authorization_code', 'refresh_token'], - scopes: [...AGENT_OAUTH_SCOPES], + scopes: [...OAUTH_SCOPES], resources, enforcePerClientResources: false, allowDynamicClientRegistration: true, allowUnauthenticatedClientRegistration: true, clientRegistrationRequirePKCE: true, - clientRegistrationAllowedScopes: [...AGENT_OAUTH_SCOPES], - clientRegistrationDefaultScopes: [...AGENT_OAUTH_STANDARD_SCOPES], + clientRegistrationAllowedScopes: [...OAUTH_SCOPES], + clientRegistrationDefaultScopes: [...OAUTH_STANDARD_SCOPES], extensions: input.resourceAudience ? [externalResourceGrantExtension(input.resourceAudience)] : [], - advertisedMetadata: { scopes_supported: [...AGENT_OAUTH_SCOPES] }, + advertisedMetadata: { scopes_supported: [...OAUTH_SCOPES] }, silenceWarnings: { oauthAuthServerConfig: true, openidConfig: true, }, postLogin: { - page: '/settings/agent-access', + page: '/settings/oauth-apps', shouldRedirect: async () => false, consentReferenceId: async ({ user, session, scopes }) => { const clientScopes = scopes.filter((scope) => scope !== 'openid' && scope !== 'profile' && scope !== 'email') - const grantableScopes = new Set(AGENT_OAUTH_SCOPES) + const grantableScopes = new Set(OAUTH_SCOPES) if (clientScopes.some((scope) => !grantableScopes.has(scope))) { throw oauthError('invalid_scope', 'Scope is not grantable') } const orgId = typeof session.activeOrganizationId === 'string' ? session.activeOrganizationId : null const selectedOrgId = orgId || (await orgs.findPersonalOrg(user.id)) - if (!selectedOrgId) throw oauthError('invalid_request', 'A workspace is required for Agent OAuth') + if (!selectedOrgId) throw oauthError('invalid_request', 'A workspace is required for OAuth') const role = await orgs.getMemberRole(selectedOrgId, user.id) if (!role && selectedOrgId !== (await orgs.findPersonalOrg(user.id))) { throw new APIError('FORBIDDEN', { error: 'access_denied', - error_description: 'Workspace access is required for Agent OAuth', + error_description: 'Workspace access is required for OAuth', }) } return selectedOrgId @@ -95,7 +95,7 @@ export function createAgentOAuthProviderOptions(input: { if (!user?.id || !referenceId) return {} return { zpan_org_id: referenceId, - zpan_actor: 'agent_oauth', + zpan_actor: 'oauth', } }, } diff --git a/server/composition.ts b/server/composition.ts index 09e1fa04..78bda6ff 100644 --- a/server/composition.ts +++ b/server/composition.ts @@ -15,7 +15,6 @@ import { createZipGateway } from './adapters/gateways/zip' import { createChangelogProvider } from './adapters/providers/changelog' import { createImageDomainProviderGateway } from './adapters/providers/image-domain-provider' import { createAdminStatsRepo } from './adapters/repos/admin-stats' -import { createAgentOAuthGateway } from './adapters/repos/agent-oauth' import { createAnnouncementRepo } from './adapters/repos/announcement' import { createApiKeyGateway } from './adapters/repos/api-keys' import { createArchiveTargetFolderRepo } from './adapters/repos/archive-target-folder' @@ -35,6 +34,7 @@ import { createLicenseBindingRepo } from './adapters/repos/license-binding' import { createMatterRepo } from './adapters/repos/matter' import { createMemberCountRepo } from './adapters/repos/member-count' import { createNotificationRepo } from './adapters/repos/notification' +import { createOAuthGateway } from './adapters/repos/oauth' import { createObjectUploadSessionRepo } from './adapters/repos/object-upload-session' import { createOrgRepo } from './adapters/repos/org' import { createProfileRepo } from './adapters/repos/profile' @@ -82,7 +82,7 @@ export function createDeps(platform: Platform, options: CreateDepsOptions = {}): return { audit: createAuditRepo(db), adminStats: createAdminStatsRepo(db), - agentOAuth: createAgentOAuthGateway(), + oauth: createOAuthGateway(), announcements: createAnnouncementRepo(db), apiKeys: createApiKeyGateway(), archiveJobs: createArchiveJobsGateway(platform), diff --git a/server/db/auth-schema.test.ts b/server/db/auth-schema.test.ts index aadefa31..6f85c0e1 100644 --- a/server/db/auth-schema.test.ts +++ b/server/db/auth-schema.test.ts @@ -94,7 +94,7 @@ describe('downloaderBootstrapCredential table', () => { }) }) -describe('Agent OAuth tables', () => { +describe('OAuth tables', () => { it('declares the managed client columns and indexes', () => { const { foreignKeys, indexes } = getTableConfig(oauthClient) diff --git a/server/http/auth/auth.cf-test.ts b/server/http/auth/auth.cf-test.ts index 34e282ef..976b53ec 100644 --- a/server/http/auth/auth.cf-test.ts +++ b/server/http/auth/auth.cf-test.ts @@ -74,14 +74,14 @@ describe('[CF] Auth API', () => { expect(rejected.status).toBe(403) }) - it('completes managed Agent OAuth consent on D1', async () => { + it('completes managed OAuth consent on D1', async () => { const app = await buildApp() const signUp = await app.request('/api/auth/sign-up/email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - name: 'CF Agent OAuth', - email: `cf-agent-oauth-${Date.now()}@example.com`, + name: 'CF OAuth', + email: `cf-oauth-${Date.now()}@example.com`, password: 'password123456', }), }) @@ -108,7 +108,7 @@ describe('[CF] Auth API', () => { redirect_uri: 'https://broker.example.com/callback', response_type: 'code', scope: 'openid offline_access objects:read quota:read', - state: 'cf-agent-oauth', + state: 'cf-oauth', code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM', code_challenge_method: 'S256', }) @@ -117,9 +117,9 @@ describe('[CF] Auth API', () => { }) const consentLocation = authorize.headers.get('location') expect(authorize.status).toBe(302) - expect(consentLocation).toMatch(/^\/settings\/agent-access\?/) + expect(consentLocation).toMatch(/^\/settings\/oauth-apps\?/) - const consent = await app.request('/api/agent-oauth-consent', { + const consent = await app.request('/api/oauth-consent', { method: 'POST', headers: { Cookie: cookie, Origin: 'http://localhost', 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/server/http/agent-oauth-grants.integration.test.ts b/server/http/oauth-grants.integration.test.ts similarity index 87% rename from server/http/agent-oauth-grants.integration.test.ts rename to server/http/oauth-grants.integration.test.ts index 45475a30..a71083be 100644 --- a/server/http/agent-oauth-grants.integration.test.ts +++ b/server/http/oauth-grants.integration.test.ts @@ -1,5 +1,5 @@ -import { AGENT_OAUTH_ACCESS_TOKEN_SECONDS, AGENT_OAUTH_REFRESH_TOKEN_SECONDS } from '@shared/agent-oauth' import { AuthorizationScope } from '@shared/authorization' +import { OAUTH_ACCESS_TOKEN_SECONDS, OAUTH_REFRESH_TOKEN_SECONDS } from '@shared/oauth' import { sql } from 'drizzle-orm' import { describe, expect, it } from 'vitest' import * as authSchema from '../db/auth-schema.js' @@ -97,14 +97,14 @@ function oauthQuery() { }).toString() } -describe('Agent OAuth grants API integration', () => { +describe('OAuth grants API integration', () => { it('returns consent context for a dynamically registered application', async () => { const { app, db } = await createTestApp() await insertClient(db) const headers = await authedHeaders(app, 'agent-consent@example.com') const { orgId } = await getUserAndPersonalOrg(db, 'agent-consent@example.com') - const res = await app.request(`/api/agent-oauth-consent?oauthQuery=${encodeURIComponent(oauthQuery())}`, { + const res = await app.request(`/api/oauth-consent?oauthQuery=${encodeURIComponent(oauthQuery())}`, { headers, }) @@ -118,8 +118,8 @@ describe('Agent OAuth grants API integration', () => { standardScopes: ['openid', 'offline_access'], redirectUri: REDIRECT_URI, grantLifetime: { - accessTokenSeconds: AGENT_OAUTH_ACCESS_TOKEN_SECONDS, - refreshTokenSeconds: AGENT_OAUTH_REFRESH_TOKEN_SECONDS, + accessTokenSeconds: OAUTH_ACCESS_TOKEN_SECONDS, + refreshTokenSeconds: OAUTH_REFRESH_TOKEN_SECONDS, }, }) }) @@ -129,14 +129,14 @@ describe('Agent OAuth grants API integration', () => { await insertClient(db) const headers = await authedHeaders(app, 'agent-submit@example.com') - const res = await app.request('/api/agent-oauth-consent', { + const res = await app.request('/api/oauth-consent', { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ accept: true, oauthQuery: `client_id=${CLIENT_ID}&response_type=token` }), }) expect(res.status).toBe(400) - await expect(res.json()).resolves.toMatchObject({ error: { message: 'Invalid Agent OAuth request' } }) + await expect(res.json()).resolves.toMatchObject({ error: { message: 'Invalid OAuth request' } }) }) it('lists and revokes the current user dynamic-client grant family', async () => { @@ -146,7 +146,7 @@ describe('Agent OAuth grants API integration', () => { const { userId, orgId } = await getUserAndPersonalOrg(db, 'agent-grants@example.com') await insertGrant(db, { userId, orgId, scopes: [AuthorizationScope.OBJECTS_READ, AuthorizationScope.QUOTA_READ] }) - const list = await app.request('/api/agent-oauth-grants', { headers }) + const list = await app.request('/api/oauth-grants', { headers }) expect(list.status).toBe(200) await expect(list.json()).resolves.toEqual({ items: [ @@ -165,7 +165,7 @@ describe('Agent OAuth grants API integration', () => { ], }) - const revoke = await app.request('/api/agent-oauth-grants/grant-1', { method: 'DELETE', headers }) + const revoke = await app.request('/api/oauth-grants/grant-1', { method: 'DELETE', headers }) expect(revoke.status).toBe(204) expect(await db.select().from(authSchema.oauthConsent)).toHaveLength(0) expect(await db.select().from(authSchema.oauthAccessToken)).toHaveLength(0) @@ -177,9 +177,9 @@ describe('Agent OAuth grants API integration', () => { const { app } = await createTestApp() const headers = await authedHeaders(app, 'agent-missing-grant@example.com') - const revoke = await app.request('/api/agent-oauth-grants/missing-grant', { method: 'DELETE', headers }) + const revoke = await app.request('/api/oauth-grants/missing-grant', { method: 'DELETE', headers }) expect(revoke.status).toBe(404) - await expect(revoke.json()).resolves.toMatchObject({ error: { message: 'Agent OAuth grant not found' } }) + await expect(revoke.json()).resolves.toMatchObject({ error: { message: 'OAuth grant not found' } }) }) }) diff --git a/server/http/agent-oauth-grants.ts b/server/http/oauth-grants.ts similarity index 55% rename from server/http/agent-oauth-grants.ts rename to server/http/oauth-grants.ts index 78190171..485ff4f2 100644 --- a/server/http/agent-oauth-grants.ts +++ b/server/http/oauth-grants.ts @@ -1,30 +1,30 @@ import { OpenAPIHono, z } from '@hono/zod-openapi' import { AuthorizationScope } from '@shared/authorization' import { - agentOAuthConsentContextSchema, - agentOAuthConsentResultSchema, - agentOAuthConsentSubmitSchema, - agentOAuthGrantListSchema, + oauthConsentContextSchema, + oauthConsentResultSchema, + oauthConsentSubmitSchema, + oauthGrantListSchema, } from '@shared/schemas' import type { Env } from '../middleware/platform' -import { getAgentOAuthConsentContext } from '../usecases/agent-oauth-consent' -import { listAgentOAuthGrants, revokeAgentOAuthGrant } from '../usecases/agent-oauth-grants' +import { getOAuthConsentContext } from '../usecases/oauth-consent' +import { listOAuthGrants, revokeOAuthGrant } from '../usecases/oauth-grants' import { authRoute, errorResponse, jsonBody, jsonContent } from './openapi' const paramsSchema = z.object({ grantId: z.string().min(1) }) const consentContextQuerySchema = z.object({ oauthQuery: z.string().min(1) }) const consentContextRoute = authRoute( - { scopes: [AuthorizationScope.AGENT_OAUTH_GRANTS_CREATE] }, + { scopes: [AuthorizationScope.OAUTH_GRANTS_CREATE] }, { - operationId: 'getAgentOAuthConsentContext', - summary: 'Get pending Agent OAuth consent context', - tags: ['Agent Access'], + operationId: 'getOAuthConsentContext', + summary: 'Get pending OAuth consent context', + tags: ['OAuth Apps'], method: 'get', - path: '/agent-oauth-consent', + path: '/oauth-consent', request: { query: consentContextQuerySchema }, responses: { - 200: jsonContent(agentOAuthConsentContextSchema, 'Agent OAuth consent context'), + 200: jsonContent(oauthConsentContextSchema, 'OAuth consent context'), 400: errorResponse('Invalid OAuth request'), 403: errorResponse('Workspace access is required'), }, @@ -32,16 +32,16 @@ const consentContextRoute = authRoute( ) const consentSubmitRoute = authRoute( - { scopes: [AuthorizationScope.AGENT_OAUTH_GRANTS_CREATE] }, + { scopes: [AuthorizationScope.OAUTH_GRANTS_CREATE] }, { - operationId: 'submitAgentOAuthConsent', - summary: 'Submit Agent OAuth consent decision', - tags: ['Agent Access'], + operationId: 'submitOAuthConsent', + summary: 'Submit OAuth consent decision', + tags: ['OAuth Apps'], method: 'post', - path: '/agent-oauth-consent', - request: jsonBody(agentOAuthConsentSubmitSchema), + path: '/oauth-consent', + request: jsonBody(oauthConsentSubmitSchema), responses: { - 200: jsonContent(agentOAuthConsentResultSchema, 'Agent OAuth consent result'), + 200: jsonContent(oauthConsentResultSchema, 'OAuth consent result'), 400: errorResponse('Invalid OAuth request'), 403: errorResponse('Workspace access is required'), }, @@ -49,39 +49,39 @@ const consentSubmitRoute = authRoute( ) const listRoute = authRoute( - { scopes: [AuthorizationScope.AGENT_OAUTH_GRANTS_READ] }, + { scopes: [AuthorizationScope.OAUTH_GRANTS_READ] }, { - operationId: 'listAgentOAuthGrants', - summary: 'List Agent OAuth grants', - tags: ['Agent Access'], + operationId: 'listOAuthGrants', + summary: 'List OAuth grants', + tags: ['OAuth Apps'], method: 'get', - path: '/agent-oauth-grants', + path: '/oauth-grants', responses: { - 200: jsonContent(agentOAuthGrantListSchema, 'Agent OAuth grants'), + 200: jsonContent(oauthGrantListSchema, 'OAuth grants'), }, }, ) const revokeRoute = authRoute( - { scopes: [AuthorizationScope.AGENT_OAUTH_GRANTS_DELETE] }, + { scopes: [AuthorizationScope.OAUTH_GRANTS_DELETE] }, { - operationId: 'revokeAgentOAuthGrant', - summary: 'Revoke an Agent OAuth grant', - tags: ['Agent Access'], + operationId: 'revokeOAuthGrant', + summary: 'Revoke an OAuth grant', + tags: ['OAuth Apps'], method: 'delete', - path: '/agent-oauth-grants/{grantId}', + path: '/oauth-grants/{grantId}', request: { params: paramsSchema }, responses: { 204: { description: 'Revoked' }, - 404: errorResponse('Agent OAuth grant not found'), + 404: errorResponse('OAuth grant not found'), }, }, ) -export const agentOAuthGrants = new OpenAPIHono() +export const oauthGrants = new OpenAPIHono() .openapi(consentContextRoute, async (c) => { const { oauthQuery } = c.req.valid('query') - const context = await getAgentOAuthConsentContext(c.get('deps'), { + const context = await getOAuthConsentContext(c.get('deps'), { db: c.get('platform').db, userId: c.get('userId')!, orgId: c.get('orgId'), @@ -92,7 +92,7 @@ export const agentOAuthGrants = new OpenAPIHono() }) .openapi(consentSubmitRoute, async (c) => { const { accept, oauthQuery } = c.req.valid('json') - await getAgentOAuthConsentContext(c.get('deps'), { + await getOAuthConsentContext(c.get('deps'), { db: c.get('platform').db, userId: c.get('userId')!, orgId: c.get('orgId'), @@ -111,15 +111,15 @@ export const agentOAuthGrants = new OpenAPIHono() ) const body = await response.json().catch(() => null) if (!response.ok) return c.json(body ?? { error: response.statusText }, response.status as 400 | 403) - return c.json(agentOAuthConsentResultSchema.parse(body), 200) + return c.json(oauthConsentResultSchema.parse(body), 200) }) .openapi(listRoute, async (c) => { - const result = await listAgentOAuthGrants(c.get('deps'), c.get('platform').db, { userId: c.get('userId')! }) + const result = await listOAuthGrants(c.get('deps'), c.get('platform').db, { userId: c.get('userId')! }) return c.json(result, 200) }) .openapi(revokeRoute, async (c) => { const { grantId } = c.req.valid('param') - await revokeAgentOAuthGrant(c.get('deps'), c.get('platform').db, { + await revokeOAuthGrant(c.get('deps'), c.get('platform').db, { userId: c.get('userId')!, grantId, }) diff --git a/server/http/oauth-resource-scopes.ts b/server/http/oauth-resource-scopes.ts index 06f51436..5f2844cb 100644 --- a/server/http/oauth-resource-scopes.ts +++ b/server/http/oauth-resource-scopes.ts @@ -1,5 +1,5 @@ import { OpenAPIHono, z } from '@hono/zod-openapi' -import { AGENT_OAUTH_RESOURCE_SCOPES, AGENT_OAUTH_SCOPE_DESCRIPTIONS } from '@shared/agent-oauth' +import { OAUTH_RESOURCE_SCOPES, OAUTH_SCOPE_DESCRIPTIONS } from '@shared/oauth' import type { Env } from '../middleware/platform' import { authRoute, jsonContent } from './openapi' @@ -28,7 +28,7 @@ const route = authRoute( // security. The empty alternative truthfully documents that this catalog // endpoint itself is public. Protected business operations remain unbound so a // delegated credential hook can sign them before Restish's built-in auth runs. -const scopeCatalogSecurity: Record[] = [{ agentOAuth2: [...AGENT_OAUTH_RESOURCE_SCOPES] }, {}] +const scopeCatalogSecurity: Record[] = [{ oauth2: [...OAUTH_RESOURCE_SCOPES] }, {}] const scopeCatalogRoute = Object.assign(route, { security: scopeCatalogSecurity, 'x-mcp-ignore': true, @@ -37,9 +37,9 @@ const scopeCatalogRoute = Object.assign(route, { export const oauthResourceScopes = new OpenAPIHono().openapi(scopeCatalogRoute, (c) => c.json( { - scopes: AGENT_OAUTH_RESOURCE_SCOPES.map((value) => ({ + scopes: OAUTH_RESOURCE_SCOPES.map((value) => ({ value, - description: AGENT_OAUTH_SCOPE_DESCRIPTIONS[value], + description: OAUTH_SCOPE_DESCRIPTIONS[value], })), }, 200, diff --git a/server/http/openapi.ts b/server/http/openapi.ts index fe85449a..a678db8d 100644 --- a/server/http/openapi.ts +++ b/server/http/openapi.ts @@ -1,5 +1,5 @@ import { createRoute, type RouteConfig, type z } from '@hono/zod-openapi' -import { AGENT_OAUTH_SCOPES } from '@shared/agent-oauth' +import { OAUTH_SCOPES } from '@shared/oauth' import { errorResponseSchema } from '@shared/schemas' import { authorize, type RouteAuthorizationDeclaration, type ScopedAuthorizationPolicy } from '../middleware/authz' @@ -24,7 +24,7 @@ export const jsonBody = (schema: T) => ({ // `jsonError`; this just documents the response shape in the OpenAPI document. export const errorResponse = (description: string) => jsonContent(errorResponseSchema, description) -const AGENT_OAUTH_SCOPE_SET = new Set(AGENT_OAUTH_SCOPES) +const OAUTH_SCOPE_SET = new Set(OAUTH_SCOPES) export function authRoute

& { path: P }>( auth: RouteAuthorizationDeclaration, @@ -86,7 +86,7 @@ function openApiPolicySecurity(policy: ScopedAuthorizationPolicy): Record AGENT_OAUTH_SCOPE_SET.has(scope)) + return policy.scopes.every((scope) => OAUTH_SCOPE_SET.has(scope)) } function openApiPolicyMetadata(policy: ScopedAuthorizationPolicy): Record { diff --git a/server/http/site/audit.integration.test.ts b/server/http/site/audit.integration.test.ts index 3edc73ea..e407b71d 100644 --- a/server/http/site/audit.integration.test.ts +++ b/server/http/site/audit.integration.test.ts @@ -29,6 +29,31 @@ describe('GET /api/site/audit-events — auth guards', () => { }) describe('GET /api/site/audit-events — licensed admin', () => { + it('normalizes the historical OAuth actor type', async () => { + const { app, db } = await createTestApp() + await seedProLicense(db) + const headers = await adminHeaders(app) + const { auditEvents } = await import('../../db/schema.js') + await db.insert(auditEvents).values({ + id: 'evt-legacy-oauth', + orgId: 'org-oauth', + userId: 'oauth-user', + actorType: ['agent', 'oauth'].join('_'), + actorRef: 'controller-1', + action: 'objects_list', + targetType: 'file', + targetId: null, + targetName: 'OAuth request', + metadata: null, + createdAt: new Date(), + }) + + const res = await app.request('/api/site/audit-events?action=objects_list', { headers }) + expect(res.status).toBe(200) + const body = (await res.json()) as { items: Array<{ actorType: string; user: { name: string } }> } + expect(body.items[0]).toMatchObject({ actorType: 'oauth', user: { name: 'OAuth:controller-1' } }) + }) + it('returns an empty list when no events match the filter [spec: audit/empty]', async () => { const { app, db } = await createTestApp() await seedProLicense(db) diff --git a/server/http/site/audit.ts b/server/http/site/audit.ts index b07a26c9..17d1838d 100644 --- a/server/http/site/audit.ts +++ b/server/http/site/audit.ts @@ -13,7 +13,7 @@ const auditEventSchema = z id: z.string(), orgId: z.string(), userId: z.string().nullable(), - actorType: z.enum(['user', 'api_key', 'agent_oauth', 'agent', 'anonymous', 'system', 'downloader', 'task-upload']), + actorType: z.enum(['user', 'api_key', 'oauth', 'agent', 'anonymous', 'system', 'downloader', 'task-upload']), actorRef: z.string().nullable(), actorIssuer: z.string().nullable(), action: z.string(), diff --git a/server/http/teams.ts b/server/http/teams.ts index 40829e9c..e74f0e89 100644 --- a/server/http/teams.ts +++ b/server/http/teams.ts @@ -75,7 +75,7 @@ const activityEventSchema = z id: z.string(), orgId: z.string(), userId: z.string().nullable(), - actorType: z.enum(['user', 'api_key', 'agent_oauth', 'agent', 'anonymous', 'system', 'downloader', 'task-upload']), + actorType: z.enum(['user', 'api_key', 'oauth', 'agent', 'anonymous', 'system', 'downloader', 'task-upload']), actorRef: z.string().nullable(), actorIssuer: z.string().nullable(), action: z.string(), diff --git a/server/middleware/audit-actor.test.ts b/server/middleware/audit-actor.test.ts index 2cd09d61..735c8748 100644 --- a/server/middleware/audit-actor.test.ts +++ b/server/middleware/audit-actor.test.ts @@ -3,9 +3,9 @@ import { auditActor } from './audit-actor' import type { AuthPrincipal } from './platform' describe('auditActor', () => { - it('records Agent OAuth principals as delegated Agent actors', () => { + it('records OAuth principals as delegated Agent actors', () => { const principal: AuthPrincipal = { - kind: 'agent-oauth', + kind: 'oauth', userId: 'user-1', actorIssuer: 'https://id.realmroot.dev/api/auth', actorSubject: 'agt_agent-1', @@ -17,7 +17,7 @@ describe('auditActor', () => { expect(auditActor(principal)).toEqual({ userId: 'user-1', - actorType: 'agent_oauth', + actorType: 'oauth', actorRef: 'agt_agent-1', actorIssuer: 'https://id.realmroot.dev/api/auth', }) diff --git a/server/middleware/audit-actor.ts b/server/middleware/audit-actor.ts index 9d77bdc8..1c0041e9 100644 --- a/server/middleware/audit-actor.ts +++ b/server/middleware/audit-actor.ts @@ -11,10 +11,10 @@ export function auditActor(principal: AuthPrincipal | null): AuditActor { if (principal.kind === 'api-key') { return { userId: principal.userId, actorType: 'api_key', actorRef: principal.keyId, actorIssuer: null } } - if (principal.kind === 'agent-oauth') { + if (principal.kind === 'oauth') { return { userId: principal.userId, - actorType: 'agent_oauth', + actorType: 'oauth', actorRef: principal.actorSubject, actorIssuer: principal.actorIssuer, } diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts index 78755424..937f136d 100644 --- a/server/middleware/auth.ts +++ b/server/middleware/auth.ts @@ -47,14 +47,14 @@ export const authMiddleware = createMiddleware(async (c, next) => { } if ( typeof payload.jti !== 'string' || - (await c.get('deps').agentOAuth.isJwtAccessTokenRevoked(c.get('platform').db, payload.jti)) + (await c.get('deps').oauth.isJwtAccessTokenRevoked(c.get('platform').db, payload.jti)) ) { throw dpopUnauthorized(audience) } if (await c.get('deps').userAdmin.isBanned(userId)) throw unauthorized('Unauthorized') const scopes = typeof payload.scope === 'string' ? payload.scope.split(/\s+/).filter(isAuthorizationScope) : [] c.set('principal', { - kind: 'agent-oauth', + kind: 'oauth', actorIssuer, actorSubject, clientId, @@ -64,11 +64,11 @@ export const authMiddleware = createMiddleware(async (c, next) => { authMethod: 'dpop', }) c.set('authzContext', { - credential: 'agent_oauth', + credential: 'oauth', userId, workspace: { mode: 'bound', orgId }, grantedScopes: new Set(scopes), - actor: { type: 'agent_oauth', ref: actorSubject, issuer: actorIssuer }, + actor: { type: 'oauth', ref: actorSubject, issuer: actorIssuer }, state: { clientId }, }) c.set('userId', userId) diff --git a/server/middleware/platform.ts b/server/middleware/platform.ts index adf0936b..f66082da 100644 --- a/server/middleware/platform.ts +++ b/server/middleware/platform.ts @@ -53,7 +53,7 @@ export type AuthPrincipal = authMethod: 'api-key' } | { - kind: 'agent-oauth' + kind: 'oauth' actorIssuer: string actorSubject: string clientId: string @@ -115,11 +115,11 @@ export type AuthzContext = state: { configId: string; enabled: true } } | { - credential: 'agent_oauth' + credential: 'oauth' userId: string workspace: { mode: 'bound'; orgId: string } grantedScopes: ReadonlySet - actor: { type: 'agent_oauth'; ref: string; issuer: string } + actor: { type: 'oauth'; ref: string; issuer: string } state: { clientId: string } } | { diff --git a/server/openapi.test.ts b/server/openapi.test.ts index 653d09cf..3b0655ca 100644 --- a/server/openapi.test.ts +++ b/server/openapi.test.ts @@ -144,7 +144,7 @@ describe('global OpenAPI document', () => { 'x-cli-config'?: unknown } - expect(doc.components?.securitySchemes?.agentOAuth2).toMatchObject({ + expect(doc.components?.securitySchemes?.oauth2).toMatchObject({ type: 'oauth2', flows: { authorizationCode: { @@ -197,7 +197,7 @@ describe('global OpenAPI document', () => { expect(document.paths['/api/oauth-resource-scopes']?.get).toMatchObject({ security: [ { - agentOAuth2: expect.arrayContaining([ + oauth2: expect.arrayContaining([ AuthorizationScope.OBJECTS_READ, AuthorizationScope.OBJECTS_CREATE, AuthorizationScope.OBJECTS_UPDATE, @@ -450,8 +450,8 @@ describe('global OpenAPI document', () => { } const ignoredOperations = [ - doc.paths['/api/agent-oauth-grants']?.get, - doc.paths['/api/agent-oauth-grants/{grantId}']?.delete, + doc.paths['/api/oauth-grants']?.get, + doc.paths['/api/oauth-grants/{grantId}']?.delete, doc.paths['/api/site/storages']?.post, doc.paths['/api/auth/sign-in/email']?.post, doc.paths['/api/auth/sign-out']?.post, diff --git a/server/scripts/backfill-agent-oauth-scopes.test.ts b/server/scripts/backfill-oauth-scopes.test.ts similarity index 72% rename from server/scripts/backfill-agent-oauth-scopes.test.ts rename to server/scripts/backfill-oauth-scopes.test.ts index ea82dc79..c72099ec 100644 --- a/server/scripts/backfill-agent-oauth-scopes.test.ts +++ b/server/scripts/backfill-oauth-scopes.test.ts @@ -4,12 +4,12 @@ import { join } from 'node:path' import Database from 'better-sqlite3' import { afterEach, describe, expect, it, vi } from 'vitest' import { - buildAgentOAuthScopeBackfill, - parseAgentOAuthScopeBackfillOptions, - runAgentOAuthScopeBackfill, -} from '../../scripts/backfill-agent-oauth-scopes' -import { AGENT_OAUTH_SCOPES } from '../../shared/agent-oauth' + buildOAuthScopeBackfill, + parseOAuthScopeBackfillOptions, + runOAuthScopeBackfill, +} from '../../scripts/backfill-oauth-scopes' import { AuthorizationScope } from '../../shared/authorization' +import { OAUTH_SCOPES } from '../../shared/oauth' const tempDirs: string[] = [] @@ -18,7 +18,7 @@ afterEach(() => { }) function createScopeDatabase() { - const dir = mkdtempSync(join(tmpdir(), 'zpan-agent-oauth-backfill-')) + const dir = mkdtempSync(join(tmpdir(), 'zpan-oauth-backfill-')) tempDirs.push(dir) const path = join(dir, 'zpan.db') const db = new Database(path) @@ -29,9 +29,9 @@ function createScopeDatabase() { return { db, path } } -describe('buildAgentOAuthScopeBackfill', () => { +describe('buildOAuthScopeBackfill', () => { it('updates ZPan resources and upload clients without expanding read-only clients', () => { - const changes = buildAgentOAuthScopeBackfill( + const changes = buildOAuthScopeBackfill( [ { id: 'zpan-resource', @@ -57,7 +57,7 @@ describe('buildAgentOAuthScopeBackfill', () => { ) expect(changes).toEqual({ - resources: [{ id: 'zpan-resource', scopes: JSON.stringify(AGENT_OAUTH_SCOPES) }], + resources: [{ id: 'zpan-resource', scopes: JSON.stringify(OAUTH_SCOPES) }], clients: [ { id: 'upload-client', @@ -68,8 +68,8 @@ describe('buildAgentOAuthScopeBackfill', () => { }) it('is idempotent after the scopes are current', () => { - const changes = buildAgentOAuthScopeBackfill( - [{ id: 'zpan-resource', name: 'ZPan API', allowedScopes: JSON.stringify(AGENT_OAUTH_SCOPES) }], + const changes = buildOAuthScopeBackfill( + [{ id: 'zpan-resource', name: 'ZPan API', allowedScopes: JSON.stringify(OAUTH_SCOPES) }], [ { id: 'upload-client', @@ -82,26 +82,26 @@ describe('buildAgentOAuthScopeBackfill', () => { }) it('rejects malformed client scope documents', () => { - expect(() => buildAgentOAuthScopeBackfill([], [{ id: 'bad', scopes: '{' }])).toThrow(SyntaxError) - expect(() => buildAgentOAuthScopeBackfill([], [{ id: 'bad', scopes: '["objects:create", 1]' }])).toThrow( + expect(() => buildOAuthScopeBackfill([], [{ id: 'bad', scopes: '{' }])).toThrow(SyntaxError) + expect(() => buildOAuthScopeBackfill([], [{ id: 'bad', scopes: '["objects:create", 1]' }])).toThrow( 'invalid_oauth_client_scopes', ) - expect(buildAgentOAuthScopeBackfill([], [{ id: 'empty', scopes: null }])).toEqual({ resources: [], clients: [] }) + expect(buildOAuthScopeBackfill([], [{ id: 'empty', scopes: null }])).toEqual({ resources: [], clients: [] }) }) it('parses sqlite and D1 targets and rejects ambiguous invocations', () => { - expect(parseAgentOAuthScopeBackfillOptions(['--sqlite', '/tmp/zpan.db', '--apply'])).toEqual({ + expect(parseOAuthScopeBackfillOptions(['--sqlite', '/tmp/zpan.db', '--apply'])).toEqual({ apply: true, target: { kind: 'sqlite', path: '/tmp/zpan.db' }, }) - expect(parseAgentOAuthScopeBackfillOptions(['--d1', 'zpan-db', '--remote', '--env', 'staging'])).toEqual({ + expect(parseOAuthScopeBackfillOptions(['--d1', 'zpan-db', '--remote', '--env', 'staging'])).toEqual({ apply: false, target: { kind: 'd1', database: 'zpan-db', remote: true, env: 'staging' }, }) - expect(() => parseAgentOAuthScopeBackfillOptions([])).toThrow('Usage:') - expect(() => parseAgentOAuthScopeBackfillOptions(['--sqlite', 'a', '--d1', 'b'])).toThrow('Usage:') - expect(() => parseAgentOAuthScopeBackfillOptions(['--sqlite'])).toThrow('Usage:') - expect(() => parseAgentOAuthScopeBackfillOptions(['--d1'])).toThrow('Usage:') + expect(() => parseOAuthScopeBackfillOptions([])).toThrow('Usage:') + expect(() => parseOAuthScopeBackfillOptions(['--sqlite', 'a', '--d1', 'b'])).toThrow('Usage:') + expect(() => parseOAuthScopeBackfillOptions(['--sqlite'])).toThrow('Usage:') + expect(() => parseOAuthScopeBackfillOptions(['--d1'])).toThrow('Usage:') }) it('dry-runs and applies the SQLite backfill end to end', () => { @@ -118,13 +118,13 @@ describe('buildAgentOAuthScopeBackfill', () => { db.close() const logs: string[] = [] - runAgentOAuthScopeBackfill(['--sqlite', path], (message) => logs.push(message)) + runOAuthScopeBackfill(['--sqlite', path], (message) => logs.push(message)) expect(JSON.parse(logs[0])).toEqual({ mode: 'dry-run', resources: 1, clients: 1 }) - runAgentOAuthScopeBackfill(['--sqlite', path, '--apply'], (message) => logs.push(message)) + runOAuthScopeBackfill(['--sqlite', path, '--apply'], (message) => logs.push(message)) const after = new Database(path, { readonly: true }) expect(after.prepare('SELECT allowed_scopes FROM oauthResource WHERE id = ?').pluck().get('zpan-resource')).toBe( - JSON.stringify(AGENT_OAUTH_SCOPES), + JSON.stringify(OAUTH_SCOPES), ) expect(after.prepare('SELECT scopes FROM oauthClient WHERE id = ?').pluck().get('upload-client')).toBe( JSON.stringify([AuthorizationScope.OBJECTS_CREATE, AuthorizationScope.QUOTA_PURCHASE]), @@ -134,7 +134,7 @@ describe('buildAgentOAuthScopeBackfill', () => { }) it('reads and applies a remote D1 backfill with escaped identifiers', () => { - const resourceScopes = JSON.stringify(AGENT_OAUTH_SCOPES) + const resourceScopes = JSON.stringify(OAUTH_SCOPES) const clientScopes = JSON.stringify([AuthorizationScope.OBJECTS_CREATE, AuthorizationScope.QUOTA_PURCHASE]) const execute = vi .fn() @@ -151,7 +151,7 @@ describe('buildAgentOAuthScopeBackfill', () => { ) .mockReturnValueOnce(JSON.stringify([{ results: [{ id: 'client-1', scopes: clientScopes }] }])) - runAgentOAuthScopeBackfill(['--d1', 'zpan-db', '--remote', '--env', 'production', '--apply'], () => {}, execute) + runOAuthScopeBackfill(['--d1', 'zpan-db', '--remote', '--env', 'production', '--apply'], () => {}, execute) expect(execute).toHaveBeenCalledTimes(6) expect(execute.mock.calls[0]?.[0]).toEqual({ diff --git a/server/usecases/deps.ts b/server/usecases/deps.ts index 798fec19..0c8d251e 100644 --- a/server/usecases/deps.ts +++ b/server/usecases/deps.ts @@ -4,7 +4,6 @@ import type { AdminStatsRepo, - AgentOAuthGateway, AnnouncementRepo, ApiKeyGateway, ArchiveJobsGateway, @@ -31,6 +30,7 @@ import type { MatterRepo, MemberCountRepo, NotificationRepo, + OAuthGateway, ObjectUploadSessionRepo, OrgRepo, ProfileRepo, @@ -58,7 +58,7 @@ import type { export interface Deps { audit: AuditRepo adminStats: AdminStatsRepo - agentOAuth: AgentOAuthGateway + oauth: OAuthGateway announcements: AnnouncementRepo apiKeys: ApiKeyGateway archiveJobs: ArchiveJobsGateway diff --git a/server/usecases/agent-oauth-consent.test.ts b/server/usecases/oauth-consent.test.ts similarity index 78% rename from server/usecases/agent-oauth-consent.test.ts rename to server/usecases/oauth-consent.test.ts index fc6806a1..88e8f978 100644 --- a/server/usecases/agent-oauth-consent.test.ts +++ b/server/usecases/oauth-consent.test.ts @@ -1,7 +1,7 @@ import { AuthorizationScope } from '@shared/authorization' import { describe, expect, it, vi } from 'vitest' -import { getAgentOAuthConsentContext } from './agent-oauth-consent' -import type { AgentOAuthGateway, OrgRepo } from './ports' +import { getOAuthConsentContext } from './oauth-consent' +import type { OAuthGateway, OrgRepo } from './ports' const db = {} as never const CLIENT_ID = 'dynamic-client' @@ -14,7 +14,6 @@ function org(overrides: Partial = {}): OrgRepo { getOrgNames: vi.fn(async () => new Map([['org-1', 'Personal']])), canReadOrg: vi.fn(async () => true), canWriteToOrg: vi.fn(), - canManageAgentAccess: vi.fn(), isPersonalOrg: vi.fn(), ...overrides, } @@ -31,7 +30,7 @@ function deps( ) { return { org: orgRepo, - agentOAuth: { + oauth: { findClient: vi.fn(async () => ({ clientId: client.clientId ?? CLIENT_ID, clientName: client.clientName ?? CLIENT_NAME, @@ -45,7 +44,7 @@ function deps( AuthorizationScope.QUOTA_READ, ], })), - } as unknown as AgentOAuthGateway, + } as unknown as OAuthGateway, } } @@ -59,14 +58,14 @@ function oauthQuery(overrides: Record = {}) { }).toString() } -describe('Agent OAuth consent usecase', () => { +describe('OAuth consent usecase', () => { it('resolves a dynamically registered client instead of hard-coding its identity', async () => { const dynamicQuery = oauthQuery({ client_id: 'dynamic-client', redirect_uri: 'https://broker.example.com/oauth/callback', }) await expect( - getAgentOAuthConsentContext( + getOAuthConsentContext( deps(org(), { clientId: 'dynamic-client', clientName: 'Broker', @@ -76,7 +75,7 @@ describe('Agent OAuth consent usecase', () => { db, userId: 'user-1', orgId: 'org-1', - requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + requestUrl: 'https://zpan.example.test/api/oauth-consent', oauthQuery: dynamicQuery, }, ), @@ -89,11 +88,11 @@ describe('Agent OAuth consent usecase', () => { it('builds server-owned consent context for the active workspace', async () => { await expect( - getAgentOAuthConsentContext(deps(org()), { + getOAuthConsentContext(deps(org()), { db, userId: 'user-1', orgId: 'org-1', - requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + requestUrl: 'https://zpan.example.test/api/oauth-consent', oauthQuery: oauthQuery(), }), ).resolves.toEqual({ @@ -113,11 +112,11 @@ describe('Agent OAuth consent usecase', () => { it('keeps the active workspace id when the workspace name is unavailable', async () => { await expect( - getAgentOAuthConsentContext(deps(org({ getOrgNames: vi.fn(async () => new Map()) })), { + getOAuthConsentContext(deps(org({ getOrgNames: vi.fn(async () => new Map()) })), { db, userId: 'user-1', orgId: 'org-1', - requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + requestUrl: 'https://zpan.example.test/api/oauth-consent', oauthQuery: oauthQuery(), }), ).resolves.toMatchObject({ @@ -127,11 +126,11 @@ describe('Agent OAuth consent usecase', () => { it('rejects requests that are not the managed authorization-code client flow', async () => { await expect( - getAgentOAuthConsentContext(deps(org()), { + getOAuthConsentContext(deps(org()), { db, userId: 'user-1', orgId: 'org-1', - requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + requestUrl: 'https://zpan.example.test/api/oauth-consent', oauthQuery: oauthQuery({ response_type: 'token' }), }), ).rejects.toMatchObject({ httpStatus: 400 }) @@ -139,21 +138,21 @@ describe('Agent OAuth consent usecase', () => { it('rejects untrusted redirect URIs and non-grantable scopes', async () => { await expect( - getAgentOAuthConsentContext(deps(org()), { + getOAuthConsentContext(deps(org()), { db, userId: 'user-1', orgId: 'org-1', - requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + requestUrl: 'https://zpan.example.test/api/oauth-consent', oauthQuery: oauthQuery({ redirect_uri: 'https://evil.example/callback' }), }), ).rejects.toMatchObject({ httpStatus: 400 }) await expect( - getAgentOAuthConsentContext(deps(org()), { + getOAuthConsentContext(deps(org()), { db, userId: 'user-1', orgId: 'org-1', - requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + requestUrl: 'https://zpan.example.test/api/oauth-consent', oauthQuery: oauthQuery({ scope: 'objects:purge' }), }), ).rejects.toMatchObject({ httpStatus: 400 }) @@ -161,11 +160,11 @@ describe('Agent OAuth consent usecase', () => { it('rejects missing or inaccessible workspaces', async () => { await expect( - getAgentOAuthConsentContext(deps(org({ canReadOrg: vi.fn(async () => false) })), { + getOAuthConsentContext(deps(org({ canReadOrg: vi.fn(async () => false) })), { db, userId: 'user-1', orgId: 'org-1', - requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + requestUrl: 'https://zpan.example.test/api/oauth-consent', oauthQuery: oauthQuery(), }), ).rejects.toMatchObject({ httpStatus: 403 }) diff --git a/server/usecases/agent-oauth-consent.ts b/server/usecases/oauth-consent.ts similarity index 63% rename from server/usecases/agent-oauth-consent.ts rename to server/usecases/oauth-consent.ts index 9125fc8d..1dc040ad 100644 --- a/server/usecases/agent-oauth-consent.ts +++ b/server/usecases/oauth-consent.ts @@ -1,18 +1,14 @@ -import { - AGENT_OAUTH_ACCESS_TOKEN_SECONDS, - AGENT_OAUTH_REFRESH_TOKEN_SECONDS, - AGENT_OAUTH_STANDARD_SCOPES, -} from '@shared/agent-oauth' import { isAuthorizationScope } from '@shared/authorization' -import { type AgentOAuthConsentContext, type OAuthResourceScope, oauthResourceScopeSchema } from '@shared/schemas' +import { OAUTH_ACCESS_TOKEN_SECONDS, OAUTH_REFRESH_TOKEN_SECONDS, OAUTH_STANDARD_SCOPES } from '@shared/oauth' +import { type OAuthConsentContext, type OAuthResourceScope, oauthResourceScopeSchema } from '@shared/schemas' import type { Database } from '../platform/interface' import type { Deps } from './deps' import { badRequest, forbidden } from './ports' -export async function getAgentOAuthConsentContext( - deps: Pick, +export async function getOAuthConsentContext( + deps: Pick, input: { db: Database; userId: string; orgId: string | null; requestUrl: string; oauthQuery: string }, -): Promise { +): Promise { const params = new URLSearchParams(input.oauthQuery) const clientId = params.get('client_id') const redirectUri = params.get('redirect_uri') @@ -20,34 +16,32 @@ export async function getAgentOAuthConsentContext( const scopeValue = params.get('scope') ?? '' if (!clientId || responseType !== 'code' || !redirectUri) { - throw badRequest('Invalid Agent OAuth request') + throw badRequest('Invalid OAuth request') } - const client = await deps.agentOAuth.findClient(input.db, clientId) + const client = await deps.oauth.findClient(input.db, clientId) if ( !client || client.disabled || !client.responseTypes.includes('code') || !client.redirectUris.includes(redirectUri) ) { - throw badRequest('Invalid Agent OAuth redirect URI') + throw badRequest('Invalid OAuth redirect URI') } const requestedScopes = scopeValue.split(/\s+/).filter(Boolean) - const standardScopes = requestedScopes.filter((scope) => - (AGENT_OAUTH_STANDARD_SCOPES as readonly string[]).includes(scope), - ) + const standardScopes = requestedScopes.filter((scope) => (OAUTH_STANDARD_SCOPES as readonly string[]).includes(scope)) const scopes = requestedScopes.filter(isOAuthResourceScope) if ( scopes.length === 0 || requestedScopes.length !== standardScopes.length + scopes.length || requestedScopes.some((scope) => !client.scopes.includes(scope)) ) { - throw badRequest('Invalid Agent OAuth scope') + throw badRequest('Invalid OAuth scope') } const orgId = input.orgId if (!orgId || !(await deps.org.canReadOrg(input.userId, orgId))) { - throw forbidden('Workspace access is required for Agent OAuth') + throw forbidden('Workspace access is required for OAuth') } const names = await deps.org.getOrgNames([orgId]) @@ -60,8 +54,8 @@ export async function getAgentOAuthConsentContext( standardScopes, redirectUri, grantLifetime: { - accessTokenSeconds: AGENT_OAUTH_ACCESS_TOKEN_SECONDS, - refreshTokenSeconds: AGENT_OAUTH_REFRESH_TOKEN_SECONDS, + accessTokenSeconds: OAUTH_ACCESS_TOKEN_SECONDS, + refreshTokenSeconds: OAUTH_REFRESH_TOKEN_SECONDS, }, } } diff --git a/server/usecases/agent-oauth-grants.test.ts b/server/usecases/oauth-grants.test.ts similarity index 67% rename from server/usecases/agent-oauth-grants.test.ts rename to server/usecases/oauth-grants.test.ts index 5ed5fe37..0d96e924 100644 --- a/server/usecases/agent-oauth-grants.test.ts +++ b/server/usecases/oauth-grants.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from 'vitest' -import { listAgentOAuthGrants, revokeAgentOAuthGrant } from './agent-oauth-grants' -import type { AgentOAuthGateway, OrgRepo } from './ports' +import { listOAuthGrants, revokeOAuthGrant } from './oauth-grants' +import type { OAuthGateway, OrgRepo } from './ports' const db = {} as never -function gateway(overrides: Partial = {}): AgentOAuthGateway { +function gateway(overrides: Partial = {}): OAuthGateway { return { findClient: vi.fn(), listRegisteredApplications: vi.fn(), @@ -23,15 +23,14 @@ function org(overrides: Partial = {}): OrgRepo { getOrgNames: vi.fn(async () => new Map([['org-1', 'Personal']])), canReadOrg: vi.fn(), canWriteToOrg: vi.fn(), - canManageAgentAccess: vi.fn(), isPersonalOrg: vi.fn(), ...overrides, } } -describe('Agent OAuth grant usecases', () => { +describe('OAuth grant usecases', () => { it('lists grants through the gateway', async () => { - const agentOAuth = gateway({ + const oauth = gateway({ listGrants: vi.fn(async () => [ { id: 'grant-1', @@ -46,7 +45,7 @@ describe('Agent OAuth grant usecases', () => { ]), }) - await expect(listAgentOAuthGrants({ agentOAuth, org: org() }, db, { userId: 'user-1' })).resolves.toEqual({ + await expect(listOAuthGrants({ oauth, org: org() }, db, { userId: 'user-1' })).resolves.toEqual({ items: [ { id: 'grant-1', @@ -65,13 +64,11 @@ describe('Agent OAuth grant usecases', () => { }) it('throws not found when revoke does not remove a grant', async () => { - const agentOAuth = gateway({ revokeGrant: vi.fn(async () => false) }) + const oauth = gateway({ revokeGrant: vi.fn(async () => false) }) - await expect( - revokeAgentOAuthGrant({ agentOAuth }, db, { userId: 'user-1', grantId: 'missing' }), - ).rejects.toMatchObject({ + await expect(revokeOAuthGrant({ oauth }, db, { userId: 'user-1', grantId: 'missing' })).rejects.toMatchObject({ httpStatus: 404, - message: 'Agent OAuth grant not found', + message: 'OAuth grant not found', }) }) }) diff --git a/server/usecases/agent-oauth-grants.ts b/server/usecases/oauth-grants.ts similarity index 63% rename from server/usecases/agent-oauth-grants.ts rename to server/usecases/oauth-grants.ts index b18a0ef6..86341a1f 100644 --- a/server/usecases/agent-oauth-grants.ts +++ b/server/usecases/oauth-grants.ts @@ -1,23 +1,23 @@ import { - type AgentOAuthGrant as AgentOAuthGrantDTO, - agentOAuthGrantDTO, + type OAuthGrant as OAuthGrantDTO, type OAuthResourceScope, + oauthGrantDTO, oauthResourceScopeSchema, } from '@shared/schemas' import type { Database } from '../platform/interface' import type { Deps } from './deps' import { notFound } from './ports' -export async function listAgentOAuthGrants( - deps: Pick, +export async function listOAuthGrants( + deps: Pick, db: Database, input: { userId: string }, -): Promise<{ items: AgentOAuthGrantDTO[] }> { - const items = await deps.agentOAuth.listGrants(db, input.userId) +): Promise<{ items: OAuthGrantDTO[] }> { + const items = await deps.oauth.listGrants(db, input.userId) const orgNames = await deps.org.getOrgNames(items.map((item) => item.orgId)) return { items: items.map((item) => - agentOAuthGrantDTO({ + oauthGrantDTO({ ...item, scopes: item.scopes.filter(isOAuthResourceScope), workspaceName: orgNames.get(item.orgId) ?? null, @@ -30,15 +30,15 @@ function isOAuthResourceScope(scope: string): scope is OAuthResourceScope { return oauthResourceScopeSchema.safeParse(scope).success } -export async function revokeAgentOAuthGrant( - deps: Pick, +export async function revokeOAuthGrant( + deps: Pick, db: Database, input: { userId: string; grantId: string; now?: Date }, ): Promise { - const revoked = await deps.agentOAuth.revokeGrant(db, { + const revoked = await deps.oauth.revokeGrant(db, { userId: input.userId, grantId: input.grantId, now: input.now ?? new Date(), }) - if (!revoked) throw notFound('Agent OAuth grant not found') + if (!revoked) throw notFound('OAuth grant not found') } diff --git a/server/usecases/ports.ts b/server/usecases/ports.ts index abc0249a..8fa30be1 100644 --- a/server/usecases/ports.ts +++ b/server/usecases/ports.ts @@ -4,7 +4,6 @@ // resource owns its own file under ports/. export * from './ports/admin-stats' -export * from './ports/agent-oauth' export * from './ports/announcement' export * from './ports/api-keys' export * from './ports/app-error' @@ -30,6 +29,7 @@ export * from './ports/licensing-cloud' export * from './ports/matter' export * from './ports/member-count' export * from './ports/notification' +export * from './ports/oauth' export * from './ports/object-upload-session' export * from './ports/org' export * from './ports/profile' diff --git a/server/usecases/ports/audit.ts b/server/usecases/ports/audit.ts index 3898e401..36d6ce8b 100644 --- a/server/usecases/ports/audit.ts +++ b/server/usecases/ports/audit.ts @@ -3,7 +3,7 @@ export type AuditActorType = | 'user' | 'api_key' - | 'agent_oauth' + | 'oauth' | 'agent' | 'anonymous' | 'system' diff --git a/server/usecases/ports/agent-oauth.ts b/server/usecases/ports/oauth.ts similarity index 78% rename from server/usecases/ports/agent-oauth.ts rename to server/usecases/ports/oauth.ts index 3b5e2fa1..814115f5 100644 --- a/server/usecases/ports/agent-oauth.ts +++ b/server/usecases/ports/oauth.ts @@ -1,7 +1,7 @@ import type { AuthorizationScope } from '@shared/authorization' import type { Database } from '../../platform/interface' -export interface AgentOAuthGrant { +export interface OAuthGrant { id: string clientId: string clientName: string @@ -12,7 +12,7 @@ export interface AgentOAuthGrant { lastUsedAt: string | null } -export interface AgentOAuthClient { +export interface OAuthClient { clientId: string clientName: string disabled: boolean @@ -32,11 +32,11 @@ export interface RegisteredOAuthApplication { createdAt: string } -export interface AgentOAuthGateway { - findClient(db: Database, clientId: string): Promise +export interface OAuthGateway { + findClient(db: Database, clientId: string): Promise listRegisteredApplications(db: Database): Promise revokeJwtAccessToken(db: Database, token: string): Promise isJwtAccessTokenRevoked(db: Database, tokenId: string): Promise - listGrants(db: Database, userId: string): Promise + listGrants(db: Database, userId: string): Promise revokeGrant(db: Database, input: { userId: string; grantId: string; now: Date }): Promise } diff --git a/server/usecases/ports/org.ts b/server/usecases/ports/org.ts index 9978e9b5..8f299d32 100644 --- a/server/usecases/ports/org.ts +++ b/server/usecases/ports/org.ts @@ -4,6 +4,5 @@ export interface OrgRepo { getOrgNames(orgIds: string[]): Promise> canReadOrg(userId: string, orgId: string): Promise canWriteToOrg(userId: string, orgId: string): Promise - canManageAgentAccess(userId: string, orgId: string): Promise isPersonalOrg(orgId: string): Promise } diff --git a/server/usecases/site/auth-provider.test.ts b/server/usecases/site/auth-provider.test.ts index 90d0b644..a2c9e32a 100644 --- a/server/usecases/site/auth-provider.test.ts +++ b/server/usecases/site/auth-provider.test.ts @@ -2,7 +2,7 @@ import { FREE_SOCIAL_LOGIN_LIMIT } from '@shared/constants' import type { BindingState } from '@shared/types' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Database } from '../../platform/interface' -import type { AgentOAuthGateway, LicenseBindingRepo, SystemOption, SystemOptionsRepo } from '../ports' +import type { LicenseBindingRepo, OAuthGateway, SystemOption, SystemOptionsRepo } from '../ports' import { type AuthProviderDeps, deleteAuthProvider, @@ -150,10 +150,10 @@ describe('auth-provider usecase', () => { }, ] const listRegisteredApplications = vi.fn(async () => registeredApplications) - const agentOAuth = { listRegisteredApplications } as unknown as AgentOAuthGateway + const oauth = { listRegisteredApplications } as unknown as OAuthGateway const db = {} as Database - const result = await listAuthProviderSettings({ ...deps, agentOAuth }, db, listOptions) + const result = await listAuthProviderSettings({ ...deps, oauth }, db, listOptions) expect(result.items).toHaveLength(1) expect(result.registeredApplications).toEqual(registeredApplications) diff --git a/server/usecases/site/auth-provider.ts b/server/usecases/site/auth-provider.ts index 1f5c6d7b..8c89d557 100644 --- a/server/usecases/site/auth-provider.ts +++ b/server/usecases/site/auth-provider.ts @@ -22,12 +22,12 @@ import type { AuthProvider } from '@shared/types' import { hasFeature } from '../../domain/licensing' import type { Database } from '../../platform/interface' import { - type AgentOAuthGateway, type AppError, badRequest, type CacheService, featureBlocked, type LicenseBindingRepo, + type OAuthGateway, type SystemOptionsRepo, } from '../ports' import { invalidateSiteConfig } from './config-cache' @@ -108,13 +108,13 @@ export async function listAuthProviders( } export async function listAuthProviderSettings( - deps: Pick & { agentOAuth: AgentOAuthGateway }, + deps: Pick & { oauth: OAuthGateway }, db: Database, { authOrigin }: { authOrigin: string }, ) { const [{ items }, registeredApplications] = await Promise.all([ listAuthProviders(deps, { authOrigin }), - deps.agentOAuth.listRegisteredApplications(db), + deps.oauth.listRegisteredApplications(db), ]) return { items, registeredApplications } } diff --git a/server/usecases/team.test.ts b/server/usecases/team.test.ts index fe7ec4e5..99416255 100644 --- a/server/usecases/team.test.ts +++ b/server/usecases/team.test.ts @@ -90,7 +90,6 @@ function makeDeps( getOrgNames: async () => new Map(), canReadOrg: async () => false, canWriteToOrg: async () => false, - canManageAgentAccess: async () => false, isPersonalOrg: async () => false, ...overrides.org, }, diff --git a/shared/authorization.test.ts b/shared/authorization.test.ts index 9d7d795e..4cb2713a 100644 --- a/shared/authorization.test.ts +++ b/shared/authorization.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { AGENT_OAUTH_RESOURCE_SCOPES } from './agent-oauth' import { WEBDAV_API_KEY_PERMISSIONS } from './api-key-templates' import { AuthorizationScope, @@ -7,6 +6,7 @@ import { CANONICAL_AUTHORIZATION_SCOPES, scopePermissions, } from './authorization' +import { OAUTH_RESOURCE_SCOPES } from './oauth' describe('authorization scope registry', () => { it('uses lowercase resource:action scopes without wildcard semantics', () => { @@ -22,7 +22,7 @@ describe('authorization scope registry', () => { it('keeps permanent object purge out of agent-grantable scopes', () => { expect(CANONICAL_AUTHORIZATION_SCOPES).toContain(AuthorizationScope.OBJECTS_PURGE) - expect(AGENT_OAUTH_RESOURCE_SCOPES).not.toContain(AuthorizationScope.OBJECTS_PURGE) + expect(OAUTH_RESOURCE_SCOPES).not.toContain(AuthorizationScope.OBJECTS_PURGE) expect(scopePermissions([AuthorizationScope.OBJECTS_DELETE])).toEqual({ objects: ['delete'] }) }) diff --git a/shared/authorization.ts b/shared/authorization.ts index e6ffd70e..3a2b5f2a 100644 --- a/shared/authorization.ts +++ b/shared/authorization.ts @@ -15,9 +15,9 @@ export const AuthorizationScope = { DOWNLOAD_TASKS_CREATE: 'download-tasks:create', DOWNLOAD_TASKS_CANCEL: 'download-tasks:cancel', SITE_ANALYTICS_READ: 'site-analytics:read', - AGENT_OAUTH_GRANTS_READ: 'agent-oauth-grants:read', - AGENT_OAUTH_GRANTS_CREATE: 'agent-oauth-grants:create', - AGENT_OAUTH_GRANTS_DELETE: 'agent-oauth-grants:delete', + OAUTH_GRANTS_READ: 'oauth-grants:read', + OAUTH_GRANTS_CREATE: 'oauth-grants:create', + OAUTH_GRANTS_DELETE: 'oauth-grants:delete', BACKGROUND_JOBS_READ: 'background-jobs:read', BACKGROUND_JOBS_CREATE: 'background-jobs:create', BACKGROUND_JOBS_UPDATE: 'background-jobs:update', diff --git a/shared/agent-oauth.ts b/shared/oauth.ts similarity index 72% rename from shared/agent-oauth.ts rename to shared/oauth.ts index 4e5276f2..a521b243 100644 --- a/shared/agent-oauth.ts +++ b/shared/oauth.ts @@ -1,14 +1,14 @@ import { AuthorizationScope } from './authorization' -export const AGENT_OAUTH_ACCESS_TOKEN_SECONDS = 15 * 60 -export const AGENT_OAUTH_REFRESH_TOKEN_SECONDS = 30 * 24 * 60 * 60 -export const AGENT_OAUTH_ACTOR_TOKEN_SECONDS = 5 * 60 +export const OAUTH_ACCESS_TOKEN_SECONDS = 15 * 60 +export const OAUTH_REFRESH_TOKEN_SECONDS = 30 * 24 * 60 * 60 +export const OAUTH_ACTOR_TOKEN_SECONDS = 5 * 60 export const JWT_BEARER_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer' export const TOKEN_EXCHANGE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange' export const OAUTH_ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token' export const AGENT_ACTOR_RESOURCE = 'urn:zpan:oauth:agent-actor' -export const AGENT_OAUTH_STANDARD_SCOPES = ['openid', 'profile', 'email', 'offline_access'] as const -export const AGENT_OAUTH_RESOURCE_SCOPES = [ +export const OAUTH_STANDARD_SCOPES = ['openid', 'profile', 'email', 'offline_access'] as const +export const OAUTH_RESOURCE_SCOPES = [ AuthorizationScope.OBJECTS_READ, AuthorizationScope.OBJECTS_CREATE, AuthorizationScope.OBJECTS_UPDATE, @@ -20,8 +20,8 @@ export const AGENT_OAUTH_RESOURCE_SCOPES = [ AuthorizationScope.QUOTA_PURCHASE, AuthorizationScope.STORAGE_USAGE_READ, ] as const -export const AGENT_OAUTH_SCOPES = [...AGENT_OAUTH_STANDARD_SCOPES, ...AGENT_OAUTH_RESOURCE_SCOPES] as const -export const AGENT_OAUTH_SCOPE_DESCRIPTIONS: Record<(typeof AGENT_OAUTH_RESOURCE_SCOPES)[number], string> = { +export const OAUTH_SCOPES = [...OAUTH_STANDARD_SCOPES, ...OAUTH_RESOURCE_SCOPES] as const +export const OAUTH_SCOPE_DESCRIPTIONS: Record<(typeof OAUTH_RESOURCE_SCOPES)[number], string> = { [AuthorizationScope.OBJECTS_READ]: 'List, inspect, and download objects', [AuthorizationScope.OBJECTS_CREATE]: 'Create folders and upload objects', [AuthorizationScope.OBJECTS_UPDATE]: 'Rename, move, and copy objects', diff --git a/shared/schemas/agent-oauth-grants.ts b/shared/schemas/agent-oauth-grants.ts deleted file mode 100644 index 1bf0e686..00000000 --- a/shared/schemas/agent-oauth-grants.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { z } from 'zod' -import { AGENT_OAUTH_ACCESS_TOKEN_SECONDS, AGENT_OAUTH_REFRESH_TOKEN_SECONDS } from '../agent-oauth' -import { oauthResourceScopeSchema } from './oauth-resource' - -export const agentOAuthGrantStatusSchema = z.enum(['active']) -export type AgentOAuthGrantStatus = z.infer - -export const agentOAuthGrantSchema = z.object({ - id: z.string(), - clientId: z.string(), - clientName: z.string(), - userId: z.string(), - orgId: z.string(), - workspaceName: z.string().nullable(), - scopes: z.array(oauthResourceScopeSchema), - createdAt: z.string(), - lastUsedAt: z.string().nullable(), - status: agentOAuthGrantStatusSchema, -}) -export type AgentOAuthGrant = z.infer - -export const agentOAuthGrantListSchema = z.object({ items: z.array(agentOAuthGrantSchema) }) -export type AgentOAuthGrantList = z.infer - -export const agentOAuthConsentContextSchema = z.object({ - clientId: z.string(), - clientName: z.string(), - instanceOrigin: z.string(), - workspace: z.object({ - id: z.string(), - name: z.string().nullable(), - }), - scopes: z.array(oauthResourceScopeSchema), - standardScopes: z.array(z.string()), - redirectUri: z.string(), - grantLifetime: z.object({ - accessTokenSeconds: z.number().int().default(AGENT_OAUTH_ACCESS_TOKEN_SECONDS), - refreshTokenSeconds: z.number().int().default(AGENT_OAUTH_REFRESH_TOKEN_SECONDS), - }), -}) -export type AgentOAuthConsentContext = z.infer - -export const agentOAuthConsentContextRequestSchema = z.object({ - oauthQuery: z.string().min(1), -}) -export type AgentOAuthConsentContextRequest = z.infer - -export const agentOAuthConsentSubmitSchema = z.object({ - accept: z.boolean(), - oauthQuery: z.string().min(1), -}) -export type AgentOAuthConsentSubmit = z.infer - -export const agentOAuthConsentResultSchema = z.object({ - url: z.string(), -}) -export type AgentOAuthConsentResult = z.infer - -export function agentOAuthGrantDTO(input: Omit): AgentOAuthGrant { - return { - ...input, - status: 'active', - } -} diff --git a/shared/schemas/index.ts b/shared/schemas/index.ts index 65a473b7..f2890620 100644 --- a/shared/schemas/index.ts +++ b/shared/schemas/index.ts @@ -9,25 +9,6 @@ export { adminAnalyticsTrafficSchema, adminOverviewSchema, } from './admin-analytics' -export type { - AgentOAuthConsentContext, - AgentOAuthConsentContextRequest, - AgentOAuthConsentResult, - AgentOAuthConsentSubmit, - AgentOAuthGrant, - AgentOAuthGrantList, - AgentOAuthGrantStatus, -} from './agent-oauth-grants' -export { - agentOAuthConsentContextRequestSchema, - agentOAuthConsentContextSchema, - agentOAuthConsentResultSchema, - agentOAuthConsentSubmitSchema, - agentOAuthGrantDTO, - agentOAuthGrantListSchema, - agentOAuthGrantSchema, - agentOAuthGrantStatusSchema, -} from './agent-oauth-grants' export type { AnnouncementInput, AnnouncementStatus, @@ -162,6 +143,25 @@ export { } from './errors' export type { ListNotificationsQuery } from './notification' export { listNotificationsQuerySchema } from './notification' +export type { + OAuthConsentContext, + OAuthConsentContextRequest, + OAuthConsentResult, + OAuthConsentSubmit, + OAuthGrant, + OAuthGrantList, + OAuthGrantStatus, +} from './oauth-grants' +export { + oauthConsentContextRequestSchema, + oauthConsentContextSchema, + oauthConsentResultSchema, + oauthConsentSubmitSchema, + oauthGrantDTO, + oauthGrantListSchema, + oauthGrantSchema, + oauthGrantStatusSchema, +} from './oauth-grants' export type { OAuthResourceScope } from './oauth-resource' export { oauthResourceScopeLabels, oauthResourceScopeSchema } from './oauth-resource' export type { CursorPage, CursorPageQuery, Page, PageQuery } from './pagination' diff --git a/shared/schemas/oauth-grants.ts b/shared/schemas/oauth-grants.ts new file mode 100644 index 00000000..89e4c754 --- /dev/null +++ b/shared/schemas/oauth-grants.ts @@ -0,0 +1,64 @@ +import { z } from 'zod' +import { OAUTH_ACCESS_TOKEN_SECONDS, OAUTH_REFRESH_TOKEN_SECONDS } from '../oauth' +import { oauthResourceScopeSchema } from './oauth-resource' + +export const oauthGrantStatusSchema = z.enum(['active']) +export type OAuthGrantStatus = z.infer + +export const oauthGrantSchema = z.object({ + id: z.string(), + clientId: z.string(), + clientName: z.string(), + userId: z.string(), + orgId: z.string(), + workspaceName: z.string().nullable(), + scopes: z.array(oauthResourceScopeSchema), + createdAt: z.string(), + lastUsedAt: z.string().nullable(), + status: oauthGrantStatusSchema, +}) +export type OAuthGrant = z.infer + +export const oauthGrantListSchema = z.object({ items: z.array(oauthGrantSchema) }) +export type OAuthGrantList = z.infer + +export const oauthConsentContextSchema = z.object({ + clientId: z.string(), + clientName: z.string(), + instanceOrigin: z.string(), + workspace: z.object({ + id: z.string(), + name: z.string().nullable(), + }), + scopes: z.array(oauthResourceScopeSchema), + standardScopes: z.array(z.string()), + redirectUri: z.string(), + grantLifetime: z.object({ + accessTokenSeconds: z.number().int().default(OAUTH_ACCESS_TOKEN_SECONDS), + refreshTokenSeconds: z.number().int().default(OAUTH_REFRESH_TOKEN_SECONDS), + }), +}) +export type OAuthConsentContext = z.infer + +export const oauthConsentContextRequestSchema = z.object({ + oauthQuery: z.string().min(1), +}) +export type OAuthConsentContextRequest = z.infer + +export const oauthConsentSubmitSchema = z.object({ + accept: z.boolean(), + oauthQuery: z.string().min(1), +}) +export type OAuthConsentSubmit = z.infer + +export const oauthConsentResultSchema = z.object({ + url: z.string(), +}) +export type OAuthConsentResult = z.infer + +export function oauthGrantDTO(input: Omit): OAuthGrant { + return { + ...input, + status: 'active', + } +} diff --git a/shared/schemas/oauth-resource.ts b/shared/schemas/oauth-resource.ts index d805f957..962edf89 100644 --- a/shared/schemas/oauth-resource.ts +++ b/shared/schemas/oauth-resource.ts @@ -1,19 +1,19 @@ import { z } from 'zod' -import { AGENT_OAUTH_RESOURCE_SCOPES } from '../agent-oauth' import { AuthorizationScope } from '../authorization' +import { OAUTH_RESOURCE_SCOPES } from '../oauth' -export const oauthResourceScopeSchema = z.enum(AGENT_OAUTH_RESOURCE_SCOPES) +export const oauthResourceScopeSchema = z.enum(OAUTH_RESOURCE_SCOPES) export type OAuthResourceScope = z.infer export const oauthResourceScopeLabels = { - [AuthorizationScope.OBJECTS_READ]: 'settings.agentAccess.scope.objectsRead', - [AuthorizationScope.OBJECTS_CREATE]: 'settings.agentAccess.scope.objectsCreate', - [AuthorizationScope.OBJECTS_UPDATE]: 'settings.agentAccess.scope.objectsUpdate', - [AuthorizationScope.OBJECTS_DELETE]: 'settings.agentAccess.scope.objectsDelete', - [AuthorizationScope.SHARES_READ]: 'settings.agentAccess.scope.sharesRead', - [AuthorizationScope.SHARES_CREATE]: 'settings.agentAccess.scope.sharesCreate', - [AuthorizationScope.SHARES_DELETE]: 'settings.agentAccess.scope.sharesDelete', - [AuthorizationScope.QUOTA_READ]: 'settings.agentAccess.scope.quotaRead', - [AuthorizationScope.QUOTA_PURCHASE]: 'settings.agentAccess.scope.quotaPurchase', - [AuthorizationScope.STORAGE_USAGE_READ]: 'settings.agentAccess.scope.storageUsageRead', + [AuthorizationScope.OBJECTS_READ]: 'settings.oauthApps.scope.objectsRead', + [AuthorizationScope.OBJECTS_CREATE]: 'settings.oauthApps.scope.objectsCreate', + [AuthorizationScope.OBJECTS_UPDATE]: 'settings.oauthApps.scope.objectsUpdate', + [AuthorizationScope.OBJECTS_DELETE]: 'settings.oauthApps.scope.objectsDelete', + [AuthorizationScope.SHARES_READ]: 'settings.oauthApps.scope.sharesRead', + [AuthorizationScope.SHARES_CREATE]: 'settings.oauthApps.scope.sharesCreate', + [AuthorizationScope.SHARES_DELETE]: 'settings.oauthApps.scope.sharesDelete', + [AuthorizationScope.QUOTA_READ]: 'settings.oauthApps.scope.quotaRead', + [AuthorizationScope.QUOTA_PURCHASE]: 'settings.oauthApps.scope.quotaPurchase', + [AuthorizationScope.STORAGE_USAGE_READ]: 'settings.oauthApps.scope.storageUsageRead', } as const satisfies Record diff --git a/shared/types/index.ts b/shared/types/index.ts index 7dc5c887..da96107f 100644 --- a/shared/types/index.ts +++ b/shared/types/index.ts @@ -625,7 +625,7 @@ export interface Announcement { export type AuditActorType = | 'user' | 'api_key' - | 'agent_oauth' + | 'oauth' | 'agent' | 'anonymous' | 'system' diff --git a/src/components/admin/audit-activity-feed.tsx b/src/components/admin/audit-activity-feed.tsx index 91169514..b5bb7dd5 100644 --- a/src/components/admin/audit-activity-feed.tsx +++ b/src/components/admin/audit-activity-feed.tsx @@ -111,7 +111,7 @@ function AdminAuditActivityItem({ event }: { event: AdminAuditEvent }) { } function formatActor(event: AdminAuditEvent): string { - if (event.actorType === 'agent_oauth') { + if (event.actorType === 'oauth') { const identity = event.actorRef ?? 'unknown' return event.actorIssuer ? `Agent:${identity} · ${event.actorIssuer}` : `Agent:${identity}` } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a82de5cd..8a2ec367 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1133,7 +1133,7 @@ "settings.tabProfile": "Profile", "settings.tabPassword": "Password", "settings.tabApiKeys": "API Keys", - "settings.tabAgentAccess": "Agent Access", + "settings.tabOAuthApps": "OAuth Apps", "settings.tabWebDav": "WebDAV", "settings.tabImageHosting": "Image Hosting", "settings.profile.section": "Profile", @@ -1207,48 +1207,44 @@ "settings.apiKeys.revokeSuccess": "API key revoked", "settings.apiKeys.orgRequired": "Select a workspace before creating this API key.", "settings.apiKeys.manage": "Manage API Keys", - "settings.agentAccess.section": "Agent Access", - "settings.agentAccess.description": "Review and revoke delegated OAuth access to your workspaces.", - "settings.agentAccess.workspaceLabel": "Workspace", - "settings.agentAccess.scope.objectsRead": "Files: read objects", - "settings.agentAccess.scope.objectsCreate": "Files: create objects", - "settings.agentAccess.scope.objectsUpdate": "Files: update objects", - "settings.agentAccess.scope.objectsDelete": "Files: delete objects", - "settings.agentAccess.scope.sharesRead": "Shares: read shares", - "settings.agentAccess.scope.sharesCreate": "Shares: create shares", - "settings.agentAccess.scope.sharesDelete": "Shares: revoke shares", - "settings.agentAccess.scope.quotaRead": "Quota: read workspace quota", - "settings.agentAccess.scope.quotaPurchase": "Quota: purchase workspace storage capacity", - "settings.agentAccess.scope.storageUsageRead": "Storage usage: read workspace usage", - "settings.agentAccess.colWorkspace": "Workspace", - "settings.agentAccess.colScopes": "Scopes", - "settings.agentAccess.colCreated": "Created", - "settings.agentAccess.colLastUsed": "Last Used", - "settings.agentAccess.colActions": "Actions", - "settings.agentAccess.never": "Never", - "settings.agentAccess.revoke": "Revoke", - "settings.agentAccess.oauthConsentEyebrow": "Delegated OAuth access", - "settings.agentAccess.oauthConsentTitle": "Authorize Application", - "settings.agentAccess.oauthConsentDescription": "Review the exact workspace and scopes this application will receive before continuing.", - "settings.agentAccess.oauthClient": "Client", - "settings.agentAccess.oauthOrigin": "ZPan instance", - "settings.agentAccess.oauthReturn": "Return URL", - "settings.agentAccess.oauthLifetime": "Grant lifetime", - "settings.agentAccess.oauthLifetimeValue": "{{days}} days", - "settings.agentAccess.oauthScopesTitle": "Requested scopes", - "settings.agentAccess.oauthEffects": "This grant can read or change files and public shares only where the listed scopes allow it. Delete and share scopes can remove content or expose public links.", - "settings.agentAccess.oauthApprove": "Approve Access", - "settings.agentAccess.oauthDeny": "Deny", - "settings.agentAccess.oauthExpiredTitle": "OAuth request expired", - "settings.agentAccess.oauthExpiredDescription": "Start the connection again to create a fresh authorization request.", - "settings.agentAccess.oauthConsentFailed": "Could not finish OAuth consent.", - "settings.agentAccess.oauthWorkspaceFailed": "Could not switch workspace.", - "settings.agentAccess.oauthGrantsSection": "Delegated OAuth Grants", - "settings.agentAccess.oauthGrantsDescription": "Manage application OAuth grants connected to your workspaces.", - "settings.agentAccess.oauthNoGrants": "No delegated OAuth grants yet", - "settings.agentAccess.oauthGrantsError": "Could not load delegated OAuth grants.", - "settings.agentAccess.oauthGrantRevokeTitle": "Revoke OAuth Grant", - "settings.agentAccess.oauthGrantRevokeConfirm": "Revoke {{client}} access to {{workspace}}? Active sessions for this workspace will stop immediately.", + "settings.oauthApps.workspaceLabel": "Workspace", + "settings.oauthApps.scope.objectsRead": "Files: read objects", + "settings.oauthApps.scope.objectsCreate": "Files: create objects", + "settings.oauthApps.scope.objectsUpdate": "Files: update objects", + "settings.oauthApps.scope.objectsDelete": "Files: delete objects", + "settings.oauthApps.scope.sharesRead": "Shares: read shares", + "settings.oauthApps.scope.sharesCreate": "Shares: create shares", + "settings.oauthApps.scope.sharesDelete": "Shares: revoke shares", + "settings.oauthApps.scope.quotaRead": "Quota: read workspace quota", + "settings.oauthApps.scope.quotaPurchase": "Quota: purchase workspace storage capacity", + "settings.oauthApps.scope.storageUsageRead": "Storage usage: read workspace usage", + "settings.oauthApps.colWorkspace": "Workspace", + "settings.oauthApps.colScopes": "Scopes", + "settings.oauthApps.colCreated": "Created", + "settings.oauthApps.colLastUsed": "Last Used", + "settings.oauthApps.colActions": "Actions", + "settings.oauthApps.never": "Never", + "settings.oauthApps.revoke": "Revoke", + "settings.oauthApps.oauthConsentEyebrow": "Delegated OAuth access", + "settings.oauthApps.oauthConsentTitle": "Authorize Application", + "settings.oauthApps.oauthConsentDescription": "Review the exact workspace and scopes this application will receive before continuing.", + "settings.oauthApps.oauthClient": "Client", + "settings.oauthApps.oauthOrigin": "ZPan instance", + "settings.oauthApps.oauthReturn": "Return URL", + "settings.oauthApps.oauthLifetime": "Grant lifetime", + "settings.oauthApps.oauthLifetimeValue": "{{days}} days", + "settings.oauthApps.oauthApprove": "Approve Access", + "settings.oauthApps.oauthDeny": "Deny", + "settings.oauthApps.oauthExpiredTitle": "OAuth request expired", + "settings.oauthApps.oauthExpiredDescription": "Start the connection again to create a fresh authorization request.", + "settings.oauthApps.oauthConsentFailed": "Could not finish OAuth consent.", + "settings.oauthApps.oauthWorkspaceFailed": "Could not switch workspace.", + "settings.oauthApps.oauthGrantsSection": "Authorized OAuth Apps", + "settings.oauthApps.oauthGrantsDescription": "Review and revoke apps authorized to access your workspaces.", + "settings.oauthApps.oauthNoGrants": "No authorized OAuth apps yet", + "settings.oauthApps.oauthGrantsError": "Could not load delegated OAuth grants.", + "settings.oauthApps.oauthGrantRevokeTitle": "Revoke OAuth Grant", + "settings.oauthApps.oauthGrantRevokeConfirm": "Revoke {{client}} access to {{workspace}}? Active sessions for this workspace will stop immediately.", "settings.appearance.theme.description": "Choose how ZPan looks. Follows your system setting by default.", "settings.appearance.language.description": "The display language for the app.", "settings.appearance.autoSaved": "Changes apply immediately.", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 8d03274d..e30fa4b5 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1133,7 +1133,7 @@ "settings.tabProfile": "基本信息", "settings.tabPassword": "密码", "settings.tabApiKeys": "API Key", - "settings.tabAgentAccess": "Agent Access", + "settings.tabOAuthApps": "OAuth 应用", "settings.tabWebDav": "WebDAV", "settings.tabImageHosting": "图床", "settings.profile.section": "个人资料", @@ -1207,48 +1207,44 @@ "settings.apiKeys.revokeSuccess": "API Key 已撤销", "settings.apiKeys.orgRequired": "创建该 API Key 前请先选择工作区。", "settings.apiKeys.manage": "管理 API Key", - "settings.agentAccess.section": "Agent Access", - "settings.agentAccess.description": "查看并撤销应用对工作空间的 OAuth 委托访问。", - "settings.agentAccess.workspaceLabel": "工作空间", - "settings.agentAccess.scope.objectsRead": "文件:读取对象", - "settings.agentAccess.scope.objectsCreate": "文件:创建对象", - "settings.agentAccess.scope.objectsUpdate": "文件:更新对象", - "settings.agentAccess.scope.objectsDelete": "文件:删除对象", - "settings.agentAccess.scope.sharesRead": "分享:读取分享", - "settings.agentAccess.scope.sharesCreate": "分享:创建分享", - "settings.agentAccess.scope.sharesDelete": "分享:撤销分享", - "settings.agentAccess.scope.quotaRead": "配额:读取工作空间配额", - "settings.agentAccess.scope.quotaPurchase": "配额:购买工作空间存储容量", - "settings.agentAccess.scope.storageUsageRead": "存储用量:读取工作空间用量", - "settings.agentAccess.colWorkspace": "工作空间", - "settings.agentAccess.colScopes": "权限", - "settings.agentAccess.colCreated": "创建时间", - "settings.agentAccess.colLastUsed": "最近使用", - "settings.agentAccess.colActions": "操作", - "settings.agentAccess.never": "从未", - "settings.agentAccess.revoke": "撤销", - "settings.agentAccess.oauthConsentEyebrow": "委托 OAuth 访问", - "settings.agentAccess.oauthConsentTitle": "授权应用", - "settings.agentAccess.oauthConsentDescription": "继续前请确认该应用将获得的具体工作空间和权限。", - "settings.agentAccess.oauthClient": "客户端", - "settings.agentAccess.oauthOrigin": "ZPan 实例", - "settings.agentAccess.oauthReturn": "返回 URL", - "settings.agentAccess.oauthLifetime": "授权有效期", - "settings.agentAccess.oauthLifetimeValue": "{{days}} 天", - "settings.agentAccess.oauthScopesTitle": "请求权限", - "settings.agentAccess.oauthEffects": "该授权只能按列出的权限读取或更改文件与公开分享。删除和分享权限可能移除内容或公开链接。", - "settings.agentAccess.oauthApprove": "批准访问", - "settings.agentAccess.oauthDeny": "拒绝", - "settings.agentAccess.oauthExpiredTitle": "OAuth 请求已过期", - "settings.agentAccess.oauthExpiredDescription": "请重新发起连接,生成新的授权请求。", - "settings.agentAccess.oauthConsentFailed": "无法完成 OAuth 授权。", - "settings.agentAccess.oauthWorkspaceFailed": "无法切换工作空间。", - "settings.agentAccess.oauthGrantsSection": "委托 OAuth 授权", - "settings.agentAccess.oauthGrantsDescription": "管理连接到你工作空间的应用 OAuth 授权。", - "settings.agentAccess.oauthNoGrants": "暂无委托 OAuth 授权", - "settings.agentAccess.oauthGrantsError": "无法加载委托 OAuth 授权。", - "settings.agentAccess.oauthGrantRevokeTitle": "撤销 OAuth 授权", - "settings.agentAccess.oauthGrantRevokeConfirm": "撤销 {{client}} 对 {{workspace}} 的访问?该工作空间的活动会话将立即停止。", + "settings.oauthApps.workspaceLabel": "工作空间", + "settings.oauthApps.scope.objectsRead": "文件:读取对象", + "settings.oauthApps.scope.objectsCreate": "文件:创建对象", + "settings.oauthApps.scope.objectsUpdate": "文件:更新对象", + "settings.oauthApps.scope.objectsDelete": "文件:删除对象", + "settings.oauthApps.scope.sharesRead": "分享:读取分享", + "settings.oauthApps.scope.sharesCreate": "分享:创建分享", + "settings.oauthApps.scope.sharesDelete": "分享:撤销分享", + "settings.oauthApps.scope.quotaRead": "配额:读取工作空间配额", + "settings.oauthApps.scope.quotaPurchase": "配额:购买工作空间存储容量", + "settings.oauthApps.scope.storageUsageRead": "存储用量:读取工作空间用量", + "settings.oauthApps.colWorkspace": "工作空间", + "settings.oauthApps.colScopes": "权限", + "settings.oauthApps.colCreated": "创建时间", + "settings.oauthApps.colLastUsed": "最近使用", + "settings.oauthApps.colActions": "操作", + "settings.oauthApps.never": "从未", + "settings.oauthApps.revoke": "撤销", + "settings.oauthApps.oauthConsentEyebrow": "委托 OAuth 访问", + "settings.oauthApps.oauthConsentTitle": "授权应用", + "settings.oauthApps.oauthConsentDescription": "继续前请确认该应用将获得的具体工作空间和权限。", + "settings.oauthApps.oauthClient": "客户端", + "settings.oauthApps.oauthOrigin": "ZPan 实例", + "settings.oauthApps.oauthReturn": "返回 URL", + "settings.oauthApps.oauthLifetime": "授权有效期", + "settings.oauthApps.oauthLifetimeValue": "{{days}} 天", + "settings.oauthApps.oauthApprove": "批准访问", + "settings.oauthApps.oauthDeny": "拒绝", + "settings.oauthApps.oauthExpiredTitle": "OAuth 请求已过期", + "settings.oauthApps.oauthExpiredDescription": "请重新发起连接,生成新的授权请求。", + "settings.oauthApps.oauthConsentFailed": "无法完成 OAuth 授权。", + "settings.oauthApps.oauthWorkspaceFailed": "无法切换工作空间。", + "settings.oauthApps.oauthGrantsSection": "已授权的 OAuth 应用", + "settings.oauthApps.oauthGrantsDescription": "查看并撤销已获准访问你工作空间的应用。", + "settings.oauthApps.oauthNoGrants": "暂无已授权的 OAuth 应用", + "settings.oauthApps.oauthGrantsError": "无法加载委托 OAuth 授权。", + "settings.oauthApps.oauthGrantRevokeTitle": "撤销 OAuth 授权", + "settings.oauthApps.oauthGrantRevokeConfirm": "撤销 {{client}} 对 {{workspace}} 的访问?该工作空间的活动会话将立即停止。", "settings.appearance.theme.description": "选择 ZPan 的外观,默认跟随系统。", "settings.appearance.language.description": "界面显示语言。", "settings.appearance.autoSaved": "修改即时生效。", diff --git a/src/lib/api.test.ts b/src/lib/api.test.ts index d0093388..1de436f4 100644 --- a/src/lib/api.test.ts +++ b/src/lib/api.test.ts @@ -47,7 +47,6 @@ import { getAdminDashboardStorageStats, getAdminDashboardTrafficStats, getAdminOverview, - getAgentOAuthConsentContext, getAnnouncement, getBackgroundJob, getChangelog, @@ -59,6 +58,7 @@ import { getInstanceInfo, getLicenseEntitlements, getLicensingStatus, + getOAuthConsentContext, getObject, getProfile, getSession, @@ -80,7 +80,6 @@ import { listActiveAnnouncements, listAdminAnnouncements, listAdminAuditLogs, - listAgentOAuthGrants, listAnnouncements, listApiKeys, listAuthProviders, @@ -96,6 +95,7 @@ import { listIhostImages, listInviteCodes, listNotifications, + listOAuthGrants, listObjectsByPath, listOrgEntitlements, listQuotas, @@ -122,8 +122,8 @@ import { resetBrandingField, restoreObject, retryBackgroundJob, - revokeAgentOAuthGrant, revokeIhostApiKey, + revokeOAuthGrant, revokeOrgEntitlement, revokeRemoteDownloadApiKey, revokeShare, @@ -138,7 +138,7 @@ import { sendDownloaderHeartbeat, serverEventsUrl, setSharePrivacy, - submitAgentOAuthConsent, + submitOAuthConsent, testEmail, testImageDomainProvider, transferObject, @@ -3118,7 +3118,7 @@ describe('api', () => { }) }) - describe('Agent OAuth consent and grants', () => { + describe('OAuth consent and grants', () => { const sampleGrantList = { items: [ { @@ -3149,11 +3149,11 @@ describe('api', () => { } vi.mocked(fetch).mockResolvedValueOnce(makeResponse(payload)) - const result = await getAgentOAuthConsentContext('client_id=dynamic-client&scope=objects%3Aread') + const result = await getOAuthConsentContext('client_id=dynamic-client&scope=objects%3Aread') expect(result).toEqual(payload) const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] - expect(url).toContain('/api/agent-oauth-consent') + expect(url).toContain('/api/oauth-consent') expect(url).toContain('oauthQuery=client_id%3Ddynamic-client%26scope%3Dobjects%253Aread') expect(init.method).toBe('GET') }) @@ -3161,11 +3161,11 @@ describe('api', () => { it('submits full OAuth consent through the Hono RPC wrapper without sending scope overrides', async () => { vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ url: 'http://127.0.0.1:8484/callback?code=abc' })) - const result = await submitAgentOAuthConsent({ accept: true, oauthQuery: 'client_id=dynamic-client' }) + const result = await submitOAuthConsent({ accept: true, oauthQuery: 'client_id=dynamic-client' }) expect(result).toEqual({ url: 'http://127.0.0.1:8484/callback?code=abc' }) const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] - expect(url).toBe('/api/agent-oauth-consent') + expect(url).toBe('/api/oauth-consent') expect(init.method).toBe('POST') expect(init.credentials).toBe('include') expect(JSON.parse(init.body as string)).toEqual({ @@ -3184,29 +3184,29 @@ describe('api', () => { }, } as unknown as Response) - await expect(submitAgentOAuthConsent({ accept: false, oauthQuery: 'client_id=dynamic-client' })).rejects.toThrow( + await expect(submitOAuthConsent({ accept: false, oauthQuery: 'client_id=dynamic-client' })).rejects.toThrow( ApiError, ) }) - it('lists delegated Agent OAuth grants', async () => { + it('lists delegated OAuth grants', async () => { vi.mocked(fetch).mockResolvedValueOnce(makeResponse(sampleGrantList)) - const result = await listAgentOAuthGrants() + const result = await listOAuthGrants() expect(result).toEqual(sampleGrantList) const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] - expect(url).toContain('/api/agent-oauth-grants') + expect(url).toContain('/api/oauth-grants') expect(init.method).toBe('GET') }) - it('revokes delegated Agent OAuth grants with DELETE', async () => { + it('revokes delegated OAuth grants with DELETE', async () => { vi.mocked(fetch).mockResolvedValueOnce(makeResponse(null, true, 204)) - await revokeAgentOAuthGrant('grant-1') + await revokeOAuthGrant('grant-1') const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] - expect(url).toContain('/api/agent-oauth-grants/grant-1') + expect(url).toContain('/api/oauth-grants/grant-1') expect(init.method).toBe('DELETE') }) }) diff --git a/src/lib/api.ts b/src/lib/api.ts index 96ffbb4e..59c38ca0 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,11 +1,6 @@ import { type ApiKeyMetadata, ApiKeyTemplate } from '@shared/api-key-templates' import type { OAuthProviderConfig } from '@shared/oauth-providers' import type { - AgentOAuthConsentContext, - AgentOAuthConsentResult, - AgentOAuthConsentSubmit, - AgentOAuthGrant, - AgentOAuthGrantList, AllowedImageMime, AnnouncementInput, CloudCreditBalanceResponse, @@ -21,6 +16,11 @@ import type { DownloadTaskActionInput, EmailSettings, ImageDomainProviderResponse, + OAuthConsentContext, + OAuthConsentResult, + OAuthConsentSubmit, + OAuthGrant, + OAuthGrantList, PatchStorageInput, PresignObjectUploadPartsInput, PublicProfile, @@ -102,7 +102,6 @@ import { adminQuotas, adminSiteInvitations, adminTeams, - agentOAuthGrantsApi, announcementsApi, authedSharesApi, authProviders, @@ -121,6 +120,7 @@ import { licensingAdminApi, licensingApi, notificationsApi, + oauthGrantsApi, objects, publicSharesApi, publicSiteInvitations, @@ -1090,22 +1090,22 @@ export function deleteIhostConfig() { }) } -export type { AgentOAuthConsentContext, AgentOAuthConsentResult, AgentOAuthGrant, AgentOAuthGrantList } +export type { OAuthConsentContext, OAuthConsentResult, OAuthGrant, OAuthGrantList } -export function getAgentOAuthConsentContext(oauthQuery: string) { - return unwrap(agentOAuthGrantsApi['agent-oauth-consent'].$get({ query: { oauthQuery } })) +export function getOAuthConsentContext(oauthQuery: string) { + return unwrap(oauthGrantsApi['oauth-consent'].$get({ query: { oauthQuery } })) } -export function submitAgentOAuthConsent(input: AgentOAuthConsentSubmit) { - return unwrap(agentOAuthGrantsApi['agent-oauth-consent'].$post({ json: input })) +export function submitOAuthConsent(input: OAuthConsentSubmit) { + return unwrap(oauthGrantsApi['oauth-consent'].$post({ json: input })) } -export function listAgentOAuthGrants() { - return unwrap(agentOAuthGrantsApi['agent-oauth-grants'].$get()) +export function listOAuthGrants() { + return unwrap(oauthGrantsApi['oauth-grants'].$get()) } -export function revokeAgentOAuthGrant(grantId: string) { - return discard(agentOAuthGrantsApi['agent-oauth-grants'][':grantId'].$delete({ param: { grantId } })) +export function revokeOAuthGrant(grantId: string) { + return discard(oauthGrantsApi['oauth-grants'][':grantId'].$delete({ param: { grantId } })) } // Image Host API Keys (via better-auth apiKey plugin) diff --git a/src/lib/rpc.ts b/src/lib/rpc.ts index bcb782c0..bf6368cf 100644 --- a/src/lib/rpc.ts +++ b/src/lib/rpc.ts @@ -6,7 +6,6 @@ import type { AdminSiteInvitationsRoute, AdminStatsRoute, AdminTeamsRoute, - AgentOAuthGrantsRoute, AnnouncementsRoute, AuthedSharesRoute, AuthProvidersRoute, @@ -25,6 +24,7 @@ import type { LicensingAdminRoute, LicensingRoute, NotificationsRoute, + OAuthGrantsRoute, ObjectsRoute, PublicSharesRoute, PublicSiteInvitationsRoute, @@ -50,7 +50,7 @@ export const objects = hc('/api/objects', opts) export const downloadTasksApi = hc('/api/downloads/tasks', opts) export const downloaderSelfApi = hc('/api/downloads/downloaders', opts) export const trash = hc('/api/trash', opts) -export const agentOAuthGrantsApi = hc('/api', opts) +export const oauthGrantsApi = hc('/api', opts) export const storages = hc('/api/site/storages', opts) export const storageUsageApi = hc('/api/storage', opts) export const adminDownloadersApi = hc('/api/downloads/downloaders', opts) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 65eb875b..18c5030b 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -38,8 +38,8 @@ import { Route as AuthenticatedTeamsInviteRouteImport } from './routes/_authenti import { Route as AuthenticatedSettingsWebdavRouteImport } from './routes/_authenticated/settings/webdav' import { Route as AuthenticatedSettingsProfileRouteImport } from './routes/_authenticated/settings/profile' import { Route as AuthenticatedSettingsPasswordRouteImport } from './routes/_authenticated/settings/password' +import { Route as AuthenticatedSettingsOauthAppsRouteImport } from './routes/_authenticated/settings/oauth-apps' import { Route as AuthenticatedSettingsApiKeysRouteImport } from './routes/_authenticated/settings/api-keys' -import { Route as AuthenticatedSettingsAgentAccessRouteImport } from './routes/_authenticated/settings/agent-access' import { Route as AuthenticatedAdminLicensingRouteImport } from './routes/_authenticated/admin/licensing' import { Route as AuthenticatedAdminDownloadersRouteImport } from './routes/_authenticated/admin/downloaders' import { Route as AuthenticatedAdminDashboardRouteImport } from './routes/_authenticated/admin/dashboard' @@ -217,18 +217,18 @@ const AuthenticatedSettingsPasswordRoute = path: '/password', getParentRoute: () => AuthenticatedSettingsRouteRoute, } as any) +const AuthenticatedSettingsOauthAppsRoute = + AuthenticatedSettingsOauthAppsRouteImport.update({ + id: '/oauth-apps', + path: '/oauth-apps', + getParentRoute: () => AuthenticatedSettingsRouteRoute, + } as any) const AuthenticatedSettingsApiKeysRoute = AuthenticatedSettingsApiKeysRouteImport.update({ id: '/api-keys', path: '/api-keys', getParentRoute: () => AuthenticatedSettingsRouteRoute, } as any) -const AuthenticatedSettingsAgentAccessRoute = - AuthenticatedSettingsAgentAccessRouteImport.update({ - id: '/agent-access', - path: '/agent-access', - getParentRoute: () => AuthenticatedSettingsRouteRoute, - } as any) const AuthenticatedAdminLicensingRoute = AuthenticatedAdminLicensingRouteImport.update({ id: '/licensing', @@ -383,8 +383,8 @@ export interface FileRoutesByFullPath { '/admin/dashboard': typeof AuthenticatedAdminDashboardRoute '/admin/downloaders': typeof AuthenticatedAdminDownloadersRoute '/admin/licensing': typeof AuthenticatedAdminLicensingRoute - '/settings/agent-access': typeof AuthenticatedSettingsAgentAccessRoute '/settings/api-keys': typeof AuthenticatedSettingsApiKeysRoute + '/settings/oauth-apps': typeof AuthenticatedSettingsOauthAppsRoute '/settings/password': typeof AuthenticatedSettingsPasswordRoute '/settings/profile': typeof AuthenticatedSettingsProfileRoute '/settings/webdav': typeof AuthenticatedSettingsWebdavRoute @@ -434,8 +434,8 @@ export interface FileRoutesByTo { '/admin/dashboard': typeof AuthenticatedAdminDashboardRoute '/admin/downloaders': typeof AuthenticatedAdminDownloadersRoute '/admin/licensing': typeof AuthenticatedAdminLicensingRoute - '/settings/agent-access': typeof AuthenticatedSettingsAgentAccessRoute '/settings/api-keys': typeof AuthenticatedSettingsApiKeysRoute + '/settings/oauth-apps': typeof AuthenticatedSettingsOauthAppsRoute '/settings/password': typeof AuthenticatedSettingsPasswordRoute '/settings/profile': typeof AuthenticatedSettingsProfileRoute '/settings/webdav': typeof AuthenticatedSettingsWebdavRoute @@ -490,8 +490,8 @@ export interface FileRoutesById { '/_authenticated/admin/dashboard': typeof AuthenticatedAdminDashboardRoute '/_authenticated/admin/downloaders': typeof AuthenticatedAdminDownloadersRoute '/_authenticated/admin/licensing': typeof AuthenticatedAdminLicensingRoute - '/_authenticated/settings/agent-access': typeof AuthenticatedSettingsAgentAccessRoute '/_authenticated/settings/api-keys': typeof AuthenticatedSettingsApiKeysRoute + '/_authenticated/settings/oauth-apps': typeof AuthenticatedSettingsOauthAppsRoute '/_authenticated/settings/password': typeof AuthenticatedSettingsPasswordRoute '/_authenticated/settings/profile': typeof AuthenticatedSettingsProfileRoute '/_authenticated/settings/webdav': typeof AuthenticatedSettingsWebdavRoute @@ -546,8 +546,8 @@ export interface FileRouteTypes { | '/admin/dashboard' | '/admin/downloaders' | '/admin/licensing' - | '/settings/agent-access' | '/settings/api-keys' + | '/settings/oauth-apps' | '/settings/password' | '/settings/profile' | '/settings/webdav' @@ -597,8 +597,8 @@ export interface FileRouteTypes { | '/admin/dashboard' | '/admin/downloaders' | '/admin/licensing' - | '/settings/agent-access' | '/settings/api-keys' + | '/settings/oauth-apps' | '/settings/password' | '/settings/profile' | '/settings/webdav' @@ -652,8 +652,8 @@ export interface FileRouteTypes { | '/_authenticated/admin/dashboard' | '/_authenticated/admin/downloaders' | '/_authenticated/admin/licensing' - | '/_authenticated/settings/agent-access' | '/_authenticated/settings/api-keys' + | '/_authenticated/settings/oauth-apps' | '/_authenticated/settings/password' | '/_authenticated/settings/profile' | '/_authenticated/settings/webdav' @@ -901,6 +901,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedSettingsPasswordRouteImport parentRoute: typeof AuthenticatedSettingsRouteRoute } + '/_authenticated/settings/oauth-apps': { + id: '/_authenticated/settings/oauth-apps' + path: '/oauth-apps' + fullPath: '/settings/oauth-apps' + preLoaderRoute: typeof AuthenticatedSettingsOauthAppsRouteImport + parentRoute: typeof AuthenticatedSettingsRouteRoute + } '/_authenticated/settings/api-keys': { id: '/_authenticated/settings/api-keys' path: '/api-keys' @@ -908,13 +915,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedSettingsApiKeysRouteImport parentRoute: typeof AuthenticatedSettingsRouteRoute } - '/_authenticated/settings/agent-access': { - id: '/_authenticated/settings/agent-access' - path: '/agent-access' - fullPath: '/settings/agent-access' - preLoaderRoute: typeof AuthenticatedSettingsAgentAccessRouteImport - parentRoute: typeof AuthenticatedSettingsRouteRoute - } '/_authenticated/admin/licensing': { id: '/_authenticated/admin/licensing' path: '/licensing' @@ -1117,8 +1117,8 @@ const AuthenticatedAdminRouteRouteWithChildren = ) interface AuthenticatedSettingsRouteRouteChildren { - AuthenticatedSettingsAgentAccessRoute: typeof AuthenticatedSettingsAgentAccessRoute AuthenticatedSettingsApiKeysRoute: typeof AuthenticatedSettingsApiKeysRoute + AuthenticatedSettingsOauthAppsRoute: typeof AuthenticatedSettingsOauthAppsRoute AuthenticatedSettingsPasswordRoute: typeof AuthenticatedSettingsPasswordRoute AuthenticatedSettingsProfileRoute: typeof AuthenticatedSettingsProfileRoute AuthenticatedSettingsWebdavRoute: typeof AuthenticatedSettingsWebdavRoute @@ -1127,9 +1127,8 @@ interface AuthenticatedSettingsRouteRouteChildren { const AuthenticatedSettingsRouteRouteChildren: AuthenticatedSettingsRouteRouteChildren = { - AuthenticatedSettingsAgentAccessRoute: - AuthenticatedSettingsAgentAccessRoute, AuthenticatedSettingsApiKeysRoute: AuthenticatedSettingsApiKeysRoute, + AuthenticatedSettingsOauthAppsRoute: AuthenticatedSettingsOauthAppsRoute, AuthenticatedSettingsPasswordRoute: AuthenticatedSettingsPasswordRoute, AuthenticatedSettingsProfileRoute: AuthenticatedSettingsProfileRoute, AuthenticatedSettingsWebdavRoute: AuthenticatedSettingsWebdavRoute, diff --git a/src/routes/_authenticated/admin/audit.test.tsx b/src/routes/_authenticated/admin/audit.test.tsx index 7e08892d..fe79fa99 100644 --- a/src/routes/_authenticated/admin/audit.test.tsx +++ b/src/routes/_authenticated/admin/audit.test.tsx @@ -131,7 +131,7 @@ describe('AuditLogsPage filters and pagination', () => { vi.mocked(listAdminAuditLogs).mockResolvedValue( auditPage(1, [ auditEvent({ - actorType: 'agent_oauth', + actorType: 'oauth', actorRef: 'agt_agent-1', actorIssuer: 'https://id.realmroot.dev/api/auth', }), diff --git a/src/routes/_authenticated/admin/audit.tsx b/src/routes/_authenticated/admin/audit.tsx index c4f25498..5115003d 100644 --- a/src/routes/_authenticated/admin/audit.tsx +++ b/src/routes/_authenticated/admin/audit.tsx @@ -93,7 +93,7 @@ function AuditRow({ event }: { event: AdminAuditEvent }) { } function formatActor(event: AdminAuditEvent): string { - if (event.actorType === 'agent_oauth') { + if (event.actorType === 'oauth') { const identity = event.actorRef ?? 'unknown' return event.actorIssuer ? `Agent:${identity} · ${event.actorIssuer}` : `Agent:${identity}` } diff --git a/src/routes/_authenticated/settings/agent-access.tsx b/src/routes/_authenticated/settings/oauth-apps.tsx similarity index 78% rename from src/routes/_authenticated/settings/agent-access.tsx rename to src/routes/_authenticated/settings/oauth-apps.tsx index 2a93d3b0..2daf0363 100644 --- a/src/routes/_authenticated/settings/agent-access.tsx +++ b/src/routes/_authenticated/settings/oauth-apps.tsx @@ -20,17 +20,17 @@ import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { - type AgentOAuthGrant, - getAgentOAuthConsentContext, - listAgentOAuthGrants, - revokeAgentOAuthGrant, - submitAgentOAuthConsent, + getOAuthConsentContext, + listOAuthGrants, + type OAuthGrant, + revokeOAuthGrant, + submitOAuthConsent, } from '@/lib/api' import { setActive, useListOrganizations } from '@/lib/auth-client' import { redirectExternal } from '@/lib/browser-navigation' -export const Route = createFileRoute('/_authenticated/settings/agent-access')({ - component: OAuthAccessSettingsPage, +export const Route = createFileRoute('/_authenticated/settings/oauth-apps')({ + component: OAuthAppsSettingsPage, }) interface Organization { @@ -56,15 +56,15 @@ function OAuthConsentPanel({ oauthQuery, organizations }: { oauthQuery: string; const [submitError, setSubmitError] = useState(null) const consentQuery = useQuery({ queryKey: ['oauth-consent', oauthQuery], - queryFn: () => getAgentOAuthConsentContext(oauthQuery), + queryFn: () => getOAuthConsentContext(oauthQuery), enabled: !!oauthQuery, retry: false, }) const submitMutation = useMutation({ - mutationFn: (accept: boolean) => submitAgentOAuthConsent({ accept, oauthQuery }), + mutationFn: (accept: boolean) => submitOAuthConsent({ accept, oauthQuery }), onSuccess: (result) => redirectExternal(result.url), onError: (error) => - setSubmitError(error instanceof Error ? error.message : t('settings.agentAccess.oauthConsentFailed')), + setSubmitError(error instanceof Error ? error.message : t('settings.oauthApps.oauthConsentFailed')), }) async function changeWorkspace(nextOrgId: string) { @@ -75,7 +75,7 @@ function OAuthConsentPanel({ oauthQuery, organizations }: { oauthQuery: string; if (error) throw error await queryClient.invalidateQueries({ queryKey: ['oauth-consent', oauthQuery] }) } catch (error) { - toast.error(error instanceof Error ? error.message : t('settings.agentAccess.oauthWorkspaceFailed')) + toast.error(error instanceof Error ? error.message : t('settings.oauthApps.oauthWorkspaceFailed')) } finally { setSwitchingOrgId(null) } @@ -89,8 +89,8 @@ function OAuthConsentPanel({ oauthQuery, organizations }: { oauthQuery: string;

-

{t('settings.agentAccess.oauthExpiredTitle')}

-

{t('settings.agentAccess.oauthExpiredDescription')}

+

{t('settings.oauthApps.oauthExpiredTitle')}

+

{t('settings.oauthApps.oauthExpiredDescription')}

) @@ -103,31 +103,31 @@ function OAuthConsentPanel({ oauthQuery, organizations }: { oauthQuery: string;
- {t('settings.agentAccess.oauthConsentEyebrow')} + {t('settings.oauthApps.oauthConsentEyebrow')}
-

{t('settings.agentAccess.oauthConsentTitle')}

-

{t('settings.agentAccess.oauthConsentDescription')}

+

{t('settings.oauthApps.oauthConsentTitle')}

+

{t('settings.oauthApps.oauthConsentDescription')}

-
{t('settings.agentAccess.oauthClient')}
+
{t('settings.oauthApps.oauthClient')}
{context.clientName}
-
{t('settings.agentAccess.oauthOrigin')}
+
{t('settings.oauthApps.oauthOrigin')}
{context.instanceOrigin}
-
{t('settings.agentAccess.oauthReturn')}
+
{t('settings.oauthApps.oauthReturn')}
{context.redirectUri}
-
{t('settings.agentAccess.oauthLifetime')}
-
{t('settings.agentAccess.oauthLifetimeValue', { days: lifetimeDays })}
+
{t('settings.oauthApps.oauthLifetime')}
+
{t('settings.oauthApps.oauthLifetimeValue', { days: lifetimeDays })}
- +