diff --git a/cmd/internal/openapi/client.gen.go b/cmd/internal/openapi/client.gen.go index c88abada..d834374d 100644 --- a/cmd/internal/openapi/client.gen.go +++ b/cmd/internal/openapi/client.gen.go @@ -2029,41 +2029,65 @@ 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" + 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 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 ( - ListAgentOAuthGrants200JSONResponseBodyItemsScopesDownloadTasksCancel ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "download-tasks:cancel" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesDownloadTasksCreate ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "download-tasks:create" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesDownloadTasksRead ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "download-tasks:read" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesImagesUpload ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "images:upload" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsCreate ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:create" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsDelete ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:delete" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsPurge ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:purge" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsRead ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:read" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsUpdate ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:update" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesQuotaRead ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "quota:read" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesSharesCreate ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "shares:create" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesSharesDelete ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "shares:delete" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesSharesRead ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "shares:read" - ListAgentOAuthGrants200JSONResponseBodyItemsScopesStorageUsageRead ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "storage-usage:read" + ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsCreate ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:create" + ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsDelete ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:delete" + ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsRead ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:read" + ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsUpdate ListAgentOAuthGrants200JSONResponseBodyItemsScopes = "objects:update" + 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 ListAgentOAuthGrants200JSONResponseBodyItemsScopesDownloadTasksCancel: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesDownloadTasksCreate: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesDownloadTasksRead: - return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesImagesUpload: - return true case ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsCreate: return true case ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsDelete: return true - case ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsPurge: - return true case ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsRead: return true case ListAgentOAuthGrants200JSONResponseBodyItemsScopesObjectsUpdate: @@ -2083,6 +2107,21 @@ func (e ListAgentOAuthGrants200JSONResponseBodyItemsScopes) Valid() bool { } } +// 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 ChangeEmail200JSONResponseBodyMessage. const ( ChangeEmail200JSONResponseBodyMessageEmailUpdated ChangeEmail200JSONResponseBodyMessage = "Email updated" @@ -3660,37 +3699,37 @@ func (e RotateWorkspaceAgentApiKeyJSONBodyScopes) Valid() bool { // Defines values for RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes. const ( - RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesObjectsCreate RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "objects:create" - RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesObjectsDelete RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "objects:delete" - RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesObjectsRead RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "objects:read" - RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesObjectsUpdate RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "objects:update" - RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesQuotaRead RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "quota:read" - RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesSharesCreate RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "shares:create" - RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesSharesDelete RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "shares:delete" - RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesSharesRead RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "shares:read" - RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesStorageUsageRead RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "storage-usage:read" + ObjectsCreate RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "objects:create" + ObjectsDelete RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "objects:delete" + ObjectsRead RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "objects:read" + ObjectsUpdate RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "objects:update" + QuotaRead RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "quota:read" + SharesCreate RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "shares:create" + SharesDelete RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "shares:delete" + SharesRead RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "shares:read" + StorageUsageRead RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes = "storage-usage:read" ) // Valid indicates whether the value is a known member of the RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes enum. func (e RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes) Valid() bool { switch e { - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesObjectsCreate: + case ObjectsCreate: return true - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesObjectsDelete: + case ObjectsDelete: return true - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesObjectsRead: + case ObjectsRead: return true - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesObjectsUpdate: + case ObjectsUpdate: return true - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesQuotaRead: + case QuotaRead: return true - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesSharesCreate: + case SharesCreate: return true - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesSharesDelete: + case SharesDelete: return true - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesSharesRead: + case SharesRead: return true - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopesStorageUsageRead: + case StorageUsageRead: return true default: return false @@ -3699,22 +3738,22 @@ func (e RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes) Valid() bool { // Defines values for RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus. const ( - RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatusActive RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus = "active" - RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatusExpired RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus = "expired" - RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatusInaccessible RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus = "inaccessible" - RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatusRevoked RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus = "revoked" + Active RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus = "active" + Expired RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus = "expired" + Inaccessible RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus = "inaccessible" + Revoked RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus = "revoked" ) // Valid indicates whether the value is a known member of the RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus enum. func (e RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus) Valid() bool { switch e { - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatusActive: + case Active: return true - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatusExpired: + case Expired: return true - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatusInaccessible: + case Inaccessible: return true - case RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatusRevoked: + case Revoked: return true default: return false @@ -5989,9 +6028,26 @@ 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 @@ -7798,6 +7854,9 @@ type RotateWorkspaceAgentApiKey201JSONResponseBodyItemScopes string // RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus defines parameters for RotateWorkspaceAgentApiKey. type RotateWorkspaceAgentApiKey201JSONResponseBodyItemStatus string +// SubmitAgentOAuthConsentJSONRequestBody defines body for SubmitAgentOAuthConsent for application/json ContentType. +type SubmitAgentOAuthConsentJSONRequestBody SubmitAgentOAuthConsentJSONBody + // BanUserJSONRequestBody defines body for BanUser for application/json ContentType. type BanUserJSONRequestBody BanUserJSONBody @@ -8842,6 +8901,14 @@ 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) @@ -9873,6 +9940,42 @@ type ClientInterface interface { RotateWorkspaceAgentApiKey(ctx context.Context, orgId string, keyId string, body RotateWorkspaceAgentApiKeyJSONRequestBody, 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 { @@ -14493,6 +14596,96 @@ func (c *Client) RotateWorkspaceAgentApiKey(ctx context.Context, orgId string, k 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 @@ -25881,6 +26074,14 @@ 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) @@ -26912,18 +27113,102 @@ type ClientWithResponsesInterface interface { RotateWorkspaceAgentApiKeyWithResponse(ctx context.Context, orgId string, keyId string, body RotateWorkspaceAgentApiKeyJSONRequestBody, reqEditors ...RequestEditorFn) (*RotateWorkspaceAgentApiKeyResponse, 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"` - CreatedAt string `json:"createdAt"` - Id string `json:"id"` - OrgId string `json:"orgId"` - Scopes []ListAgentOAuthGrants200JSONResponseBodyItemsScopes `json:"scopes"` - UpdatedAt string `json:"updatedAt"` - UserId string `json:"userId"` + ClientId string `json:"clientId"` + ClientName *string `json:"clientName,omitempty"` + 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"` } } @@ -37771,6 +38056,32 @@ func (r RotateWorkspaceAgentApiKeyResponse) 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...) @@ -41111,6 +41422,103 @@ func (c *ClientWithResponses) RotateWorkspaceAgentApiKeyWithResponse(ctx context return ParseRotateWorkspaceAgentApiKeyResponse(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) @@ -41128,13 +41536,16 @@ func ParseListAgentOAuthGrantsResponse(rsp *http.Response) (*ListAgentOAuthGrant case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: var dest struct { Items []struct { - ClientId string `json:"clientId"` - CreatedAt string `json:"createdAt"` - Id string `json:"id"` - OrgId string `json:"orgId"` - Scopes []ListAgentOAuthGrants200JSONResponseBodyItemsScopes `json:"scopes"` - UpdatedAt string `json:"updatedAt"` - UserId string `json:"userId"` + ClientId string `json:"clientId"` + ClientName *string `json:"clientName,omitempty"` + 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 { diff --git a/e2e/agent-access.spec.ts b/e2e/agent-access.spec.ts new file mode 100644 index 00000000..cb486d32 --- /dev/null +++ b/e2e/agent-access.spec.ts @@ -0,0 +1,169 @@ +import { expect, test } from '@playwright/test' +import { signUpAndGoToFiles } from './helpers' + +const oauthQuery = + 'client_id=zpan-agent&redirect_uri=http%3A%2F%2F127.0.0.1%3A8484%2Fcallback&response_type=code&scope=openid%20offline_access%20objects%3Aread%20shares%3Acreate%20quota%3Aread' + +test.describe('Agent Access OAuth 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 route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + clientId: 'zpan-agent', + clientName: 'ZPan Agent', + instanceOrigin: 'http://localhost:5185', + workspace: { id: 'org-e2e', name: 'Personal' }, + scopes: ['objects:read', 'shares:create', 'quota:read'], + standardScopes: ['openid', 'offline_access'], + redirectUri: 'http://127.0.0.1:8484/callback', + grantLifetime: { accessTokenSeconds: 900, refreshTokenSeconds: 2_592_000 }, + }), + }) + }) + await page.route('**/api/agent-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 } + expect(body).toEqual({ accept: true, oauthQuery }) + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ url: 'http://127.0.0.1:8484/callback?code=e2e-code' }), + }) + }) + await page.route('http://127.0.0.1:8484/callback?code=e2e-code', async (route) => { + await route.fulfill({ contentType: 'text/html', body: '
Returned to Restish
' }) + }) + + await page.goto(`/settings/agent-access?${oauthQuery}`) + + await expect(page.getByRole('heading', { name: 'Authorize ZPan Agent' })).toBeVisible() + await expect(page.getByText('http://localhost:5185')).toBeVisible() + await expect(page.getByText('http://127.0.0.1:8484/callback')).toBeVisible() + await expect(page.getByText('Files: read objects')).toBeVisible() + await expect(page.getByText('Shares: create shares')).toBeVisible() + await expect(page.getByText('Quota: read workspace quota')).toBeVisible() + + await page.getByRole('button', { name: 'Approve Access' }).click() + await expect(page).toHaveURL(/127\.0\.0\.1:8484\/callback\?code=e2e-code/, { timeout: 10000 }) + await expect(page.getByText('Returned to Restish')).toBeVisible() + }) + + test('lists and revokes delegated grants in settings @desktop', async ({ page }) => { + await signUpAndGoToFiles(page) + let revoked = false + + await page.route('**/api/agent-oauth-grants', async (route) => { + if (route.request().method() !== 'GET') return route.fallback() + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + items: revoked + ? [] + : [ + { + id: 'grant-e2e', + clientId: 'zpan-agent', + clientName: 'ZPan Agent', + userId: 'user-e2e', + orgId: 'org-e2e', + workspaceName: 'Personal', + scopes: ['objects:read', 'shares:create'], + createdAt: '2026-07-29T12:00:00.000Z', + lastUsedAt: null, + status: 'active', + }, + ], + }), + }) + }) + await page.route('**/api/agent-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 expect(page.getByText('Delegated OAuth Grants')).toBeVisible() + await expect(page.getByRole('cell', { name: 'ZPan Agent' })).toBeVisible() + await expect(page.getByText('Shares: create shares')).toBeVisible() + + const revokeButtons = page.getByRole('button', { name: 'Revoke' }) + 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() + }) + + 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 route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + clientId: 'zpan-agent', + clientName: 'ZPan Agent', + instanceOrigin: 'http://localhost:5185', + workspace: { id: 'org-e2e', name: 'Personal' }, + scopes: ['objects:read', 'shares:create', 'quota:read'], + standardScopes: ['openid', 'offline_access'], + redirectUri: 'http://127.0.0.1:8484/callback', + grantLifetime: { accessTokenSeconds: 900, refreshTokenSeconds: 2_592_000 }, + }), + }) + }) + await page.route('**/api/agent-oauth-grants', async (route) => { + if (route.request().method() !== 'GET') return route.fallback() + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + items: [ + { + id: 'grant-mobile', + clientId: 'zpan-agent', + clientName: 'ZPan Agent', + userId: 'user-e2e', + orgId: 'org-e2e', + workspaceName: 'Personal', + scopes: ['objects:read', 'shares:create', 'quota:read'], + createdAt: '2026-07-29T12:00:00.000Z', + lastUsedAt: '2026-07-29T12:30:00.000Z', + status: 'active', + }, + ], + }), + }) + }) + + await page.goto(`/settings/agent-access?${oauthQuery}`) + await expect(page.getByRole('heading', { name: 'Authorize ZPan Agent' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Approve Access' })).toBeVisible() + await expect(page.getByText('Files: read objects')).toBeVisible() + await expect(page.getByText('Shares: create shares')).toBeVisible() + await expect + .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 expect(page.getByRole('cell', { name: 'ZPan Agent' })).toBeVisible() + const grantsTableContainer = page.locator('[data-slot="table-container"]').last() + await expect(grantsTableContainer).toBeVisible() + await expect + .poll(async () => + grantsTableContainer.evaluate((node) => (node as HTMLElement).scrollWidth > (node as HTMLElement).clientWidth), + ) + .toBe(true) + await expect + .poll(async () => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)) + .toBe(true) + await grantsTableContainer.evaluate((node) => { + node.scrollLeft = node.scrollWidth + }) + await expect(page.getByRole('button', { name: 'Revoke' }).last()).toBeVisible() + }) +}) diff --git a/migrations/0082_agent_oauth_consent_last_used_at.sql b/migrations/0082_agent_oauth_consent_last_used_at.sql new file mode 100644 index 00000000..946eb15b --- /dev/null +++ b/migrations/0082_agent_oauth_consent_last_used_at.sql @@ -0,0 +1 @@ +ALTER TABLE `oauthConsent` ADD `last_used_at` integer; \ No newline at end of file diff --git a/migrations/meta/0082_snapshot.json b/migrations/meta/0082_snapshot.json new file mode 100644 index 00000000..ad831c29 --- /dev/null +++ b/migrations/meta/0082_snapshot.json @@ -0,0 +1,5605 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "5bb69def-4eeb-4aaf-8540-1fe87aa31d34", + "prevId": "813569e8-2a9a-4e8b-a4ec-610fced8b2e1", + "tables": { + "announcements": { + "name": "announcements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "announcements_status_priority_idx": { + "name": "announcements_status_priority_idx", + "columns": [ + "status", + "priority" + ], + "isUnique": false + }, + "announcements_published_idx": { + "name": "announcements_published_idx", + "columns": [ + "published_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_name": { + "name": "target_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_ref": { + "name": "actor_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_events_org_created_idx": { + "name": "audit_events_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_created_idx": { + "name": "audit_events_user_created_idx", + "columns": [ + "user_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_action_created_idx": { + "name": "audit_events_action_created_idx", + "columns": [ + "action", + "created_at" + ], + "isUnique": false + }, + "audit_events_target_created_idx": { + "name": "audit_events_target_created_idx", + "columns": [ + "target_type", + "target_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_created_idx": { + "name": "audit_events_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "background_jobs": { + "name": "background_jobs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_folder": { + "name": "target_folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_path": { + "name": "target_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input_bytes": { + "name": "input_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_bytes": { + "name": "output_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "processed_bytes": { + "name": "processed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "current_filename": { + "name": "current_filename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_metadata": { + "name": "result_metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retryable": { + "name": "retryable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "cancelable": { + "name": "cancelable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "retried_from_job_id": { + "name": "retried_from_job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "background_jobs_org_created_idx": { + "name": "background_jobs_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "background_jobs_org_status_idx": { + "name": "background_jobs_org_status_idx", + "columns": [ + "org_id", + "status" + ], + "isUnique": false + }, + "background_jobs_org_type_idx": { + "name": "background_jobs_org_type_idx", + "columns": [ + "org_id", + "type" + ], + "isUnique": false + }, + "background_jobs_created_idx": { + "name": "background_jobs_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "background_jobs_org_page_idx": { + "name": "background_jobs_org_page_idx", + "columns": [ + "org_id", + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cloud_traffic_reports": { + "name": "cloud_traffic_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_bytes": { + "name": "unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credits_per_unit": { + "name": "credits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cloud_traffic_reports_event_uniq": { + "name": "cloud_traffic_reports_event_uniq", + "columns": [ + "event_id" + ], + "isUnique": true + }, + "cloud_traffic_reports_org_period_idx": { + "name": "cloud_traffic_reports_org_period_idx", + "columns": [ + "org_id", + "period" + ], + "isUnique": false + }, + "cloud_traffic_reports_status_idx": { + "name": "cloud_traffic_reports_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "cloud_traffic_reports_retry_idx": { + "name": "cloud_traffic_reports_retry_idx", + "columns": [ + "status", + "next_retry_at", + "created_at" + ], + "isUnique": false + }, + "cloud_traffic_reports_issued_idx": { + "name": "cloud_traffic_reports_issued_idx", + "columns": [ + "issued_at" + ], + "isUnique": false + }, + "cloud_traffic_reports_updated_idx": { + "name": "cloud_traffic_reports_updated_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "download_tasks": { + "name": "download_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_uri": { + "name": "source_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_folder": { + "name": "target_folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "assigned_downloader_id": { + "name": "assigned_downloader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "billing_authorized_bytes": { + "name": "billing_authorized_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_charged_bytes": { + "name": "billing_charged_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_charged_credits": { + "name": "billing_charged_credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_status": { + "name": "billing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_object_id": { + "name": "result_object_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "events": { + "name": "events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "resolve_started_at": { + "name": "resolve_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolve_completed_at": { + "name": "resolve_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "download_completed_at": { + "name": "download_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ingest_started_at": { + "name": "ingest_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ingest_completed_at": { + "name": "ingest_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seeding_started_at": { + "name": "seeding_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seeding_stopped_at": { + "name": "seeding_stopped_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "download_tasks_org_created_idx": { + "name": "download_tasks_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "download_tasks_org_status_idx": { + "name": "download_tasks_org_status_idx", + "columns": [ + "org_id", + "status" + ], + "isUnique": false + }, + "download_tasks_org_category_idx": { + "name": "download_tasks_org_category_idx", + "columns": [ + "org_id", + "category" + ], + "isUnique": false + }, + "download_tasks_org_tags_idx": { + "name": "download_tasks_org_tags_idx", + "columns": [ + "org_id", + "tags" + ], + "isUnique": false + }, + "download_tasks_downloader_idx": { + "name": "download_tasks_downloader_idx", + "columns": [ + "assigned_downloader_id", + "status" + ], + "isUnique": false + }, + "download_tasks_created_idx": { + "name": "download_tasks_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "download_tasks_finished_idx": { + "name": "download_tasks_finished_idx", + "columns": [ + "finished_at" + ], + "isUnique": false + }, + "download_tasks_org_deleted_created_idx": { + "name": "download_tasks_org_deleted_created_idx", + "columns": [ + "org_id", + "deleted_at", + "created_at" + ], + "isUnique": false + }, + "download_tasks_org_page_idx": { + "name": "download_tasks_org_page_idx", + "columns": [ + "org_id", + "deleted_at", + "created_at", + "id" + ], + "isUnique": false + }, + "download_tasks_downloader_page_idx": { + "name": "download_tasks_downloader_page_idx", + "columns": [ + "assigned_downloader_id", + "deleted_at", + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "downloaders": { + "name": "downloaders", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_jti": { + "name": "token_jti", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'offline'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "arch": { + "name": "arch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "engine": { + "name": "engine", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'http'" + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "max_concurrent_tasks": { + "name": "max_concurrent_tasks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "current_tasks": { + "name": "current_tasks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "download_bps": { + "name": "download_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "upload_bps": { + "name": "upload_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "free_disk_bytes": { + "name": "free_disk_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remote_download_credit_billing_enabled": { + "name": "remote_download_credit_billing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_download_credit_unit_bytes": { + "name": "remote_download_credit_unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 104857600 + }, + "remote_download_credit_per_unit": { + "name": "remote_download_credit_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "downloaders_token_jti_unique": { + "name": "downloaders_token_jti_unique", + "columns": [ + "token_jti" + ], + "isUnique": true + }, + "downloaders_status_idx": { + "name": "downloaders_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "downloaders_enabled_idx": { + "name": "downloaders_enabled_idx", + "columns": [ + "enabled" + ], + "isUnique": false + }, + "downloaders_created_idx": { + "name": "downloaders_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "image_hosting_configs": { + "name": "image_hosting_configs", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_provider": { + "name": "domain_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_hostname_id": { + "name": "provider_hostname_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_status": { + "name": "domain_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_error": { + "name": "domain_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_last_checked_at": { + "name": "domain_last_checked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_verified_at": { + "name": "domain_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referer_allowlist": { + "name": "referer_allowlist", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "image_hosting_configs_custom_domain_unique": { + "name": "image_hosting_configs_custom_domain_unique", + "columns": [ + "custom_domain" + ], + "isUnique": true + } + }, + "foreignKeys": { + "image_hosting_configs_org_id_organization_id_fk": { + "name": "image_hosting_configs_org_id_organization_id_fk", + "tableFrom": "image_hosting_configs", + "tableTo": "organization", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "image_hostings": { + "name": "image_hostings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime": { + "name": "mime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "purged_at": { + "name": "purged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "image_hostings_token_unique": { + "name": "image_hostings_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "image_hostings_org_path_uniq": { + "name": "image_hostings_org_path_uniq", + "columns": [ + "org_id", + "path" + ], + "isUnique": true, + "where": "\"image_hostings\".\"purged_at\" IS NULL" + }, + "image_hostings_org_created_idx": { + "name": "image_hostings_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "image_hostings_page_idx": { + "name": "image_hostings_page_idx", + "columns": [ + "org_id", + "status", + "purged_at", + "created_at", + "id" + ], + "isUnique": false + }, + "image_hostings_token_idx": { + "name": "image_hostings_token_idx", + "columns": [ + "token" + ], + "isUnique": false + } + }, + "foreignKeys": { + "image_hostings_org_id_organization_id_fk": { + "name": "image_hostings_org_id_organization_id_fk", + "tableFrom": "image_hostings", + "tableTo": "organization", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "image_hostings_storage_id_storages_id_fk": { + "name": "image_hostings_storage_id_storages_id_fk", + "tableFrom": "image_hostings", + "tableTo": "storages", + "columnsFrom": [ + "storage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite_codes": { + "name": "invite_codes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_by": { + "name": "used_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_codes_code_unique": { + "name": "invite_codes_code_unique", + "columns": [ + "code" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "license_bindings": { + "name": "license_bindings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "cloud_binding_id": { + "name": "cloud_binding_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_store_id": { + "name": "cloud_store_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_account_id": { + "name": "cloud_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_account_email": { + "name": "cloud_account_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cached_certificate": { + "name": "cached_certificate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cached_certificate_expires_at": { + "name": "cached_certificate_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_at": { + "name": "bound_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refresh_at": { + "name": "last_refresh_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refresh_error": { + "name": "last_refresh_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "license_bindings_active_uniq": { + "name": "license_bindings_active_uniq", + "columns": [ + "status" + ], + "isUnique": true, + "where": "status = 'active'" + }, + "license_bindings_cloud_binding_idx": { + "name": "license_bindings_cloud_binding_idx", + "columns": [ + "cloud_binding_id" + ], + "isUnique": false + }, + "license_bindings_instance_idx": { + "name": "license_bindings_instance_idx", + "columns": [ + "instance_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "matters": { + "name": "matters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "dirtype": { + "name": "dirtype", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "parent": { + "name": "parent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "object": { + "name": "object", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "trashed_at": { + "name": "trashed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "matters_alias_unique": { + "name": "matters_alias_unique", + "columns": [ + "alias" + ], + "isUnique": true + }, + "matters_status_dir_created_idx": { + "name": "matters_status_dir_created_idx", + "columns": [ + "status", + "dirtype", + "created_at" + ], + "isUnique": false + }, + "matters_webdav_path_idx": { + "name": "matters_webdav_path_idx", + "columns": [ + "org_id", + "parent", + "name", + "status", + "trashed_at", + "purged_at" + ], + "isUnique": false + }, + "matters_webdav_children_idx": { + "name": "matters_webdav_children_idx", + "columns": [ + "org_id", + "parent", + "status", + "trashed_at", + "purged_at", + "\"dirtype\" desc", + "name" + ], + "isUnique": false + }, + "matters_list_page_idx": { + "name": "matters_list_page_idx", + "columns": [ + "org_id", + "parent", + "status", + "trashed_at", + "purged_at", + "\"dirtype\" desc", + "created_at", + "id" + ], + "isUnique": false + }, + "matters_trash_page_idx": { + "name": "matters_trash_page_idx", + "columns": [ + "org_id", + "status", + "purged_at", + "trashed_at", + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notifications": { + "name": "notifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "ref_type": { + "name": "ref_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ref_id": { + "name": "ref_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read_at": { + "name": "read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notifications_user_created_idx": { + "name": "notifications_user_created_idx", + "columns": [ + "user_id", + "created_at" + ], + "isUnique": false + }, + "notifications_user_read_idx": { + "name": "notifications_user_read_idx", + "columns": [ + "user_id", + "read_at" + ], + "isUnique": false + }, + "notifications_user_page_idx": { + "name": "notifications_user_page_idx", + "columns": [ + "user_id", + "read_at", + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "object_upload_sessions": { + "name": "object_upload_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_id": { + "name": "object_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upload_id": { + "name": "upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "on_conflict": { + "name": "on_conflict", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fail'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "object_upload_sessions_object_idx": { + "name": "object_upload_sessions_object_idx", + "columns": [ + "org_id", + "object_id" + ], + "isUnique": false + }, + "object_upload_sessions_expires_idx": { + "name": "object_upload_sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "org_quota_entitlements": { + "name": "org_quota_entitlements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entitlement_type": { + "name": "entitlement_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'grant'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "starts_at": { + "name": "starts_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "org_quota_entitlements_org_resource_idx": { + "name": "org_quota_entitlements_org_resource_idx", + "columns": [ + "org_id", + "resource_type", + "status" + ], + "isUnique": false + }, + "org_quota_entitlements_org_type_idx": { + "name": "org_quota_entitlements_org_type_idx", + "columns": [ + "org_id", + "resource_type", + "entitlement_type", + "status" + ], + "isUnique": false + }, + "org_quota_entitlements_active_plan_uniq": { + "name": "org_quota_entitlements_active_plan_uniq", + "columns": [ + "org_id", + "resource_type", + "entitlement_type" + ], + "isUnique": true, + "where": "status = 'active' AND entitlement_type = 'plan' AND source <> 'free_plan'" + }, + "org_quota_entitlements_source_resource_uniq": { + "name": "org_quota_entitlements_source_resource_uniq", + "columns": [ + "source", + "source_id", + "resource_type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "org_quotas": { + "name": "org_quotas", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quota": { + "name": "quota", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_quota": { + "name": "traffic_quota", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_used": { + "name": "traffic_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_period": { + "name": "traffic_period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'1970-01'" + } + }, + "indexes": { + "org_quotas_org_uniq": { + "name": "org_quotas_org_uniq", + "columns": [ + "org_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "remote_download_usage_reports": { + "name": "remote_download_usage_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "downloader_id": { + "name": "downloader_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_index": { + "name": "unit_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_bytes": { + "name": "unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credits_per_unit": { + "name": "credits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "remote_download_usage_reports_event_id_unique": { + "name": "remote_download_usage_reports_event_id_unique", + "columns": [ + "event_id" + ], + "isUnique": true + }, + "remote_download_usage_task_unit_uniq": { + "name": "remote_download_usage_task_unit_uniq", + "columns": [ + "task_id", + "unit_index" + ], + "isUnique": true + }, + "remote_download_usage_org_idx": { + "name": "remote_download_usage_org_idx", + "columns": [ + "org_id" + ], + "isUnique": false + }, + "remote_download_usage_status_idx": { + "name": "remote_download_usage_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "remote_download_usage_created_idx": { + "name": "remote_download_usage_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "resource_changes": { + "name": "resource_changes", + "columns": { + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "change_type": { + "name": "change_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "resource_changes_scope_sequence_idx": { + "name": "resource_changes_scope_sequence_idx", + "columns": [ + "scope_type", + "scope_id", + "sequence" + ], + "isUnique": false + }, + "resource_changes_resource_sequence_idx": { + "name": "resource_changes_resource_sequence_idx", + "columns": [ + "resource_type", + "resource_id", + "sequence" + ], + "isUnique": false + }, + "resource_changes_occurred_idx": { + "name": "resource_changes_occurred_idx", + "columns": [ + "occurred_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "share_recipients": { + "name": "share_recipients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "share_recipients_share_id_idx": { + "name": "share_recipients_share_id_idx", + "columns": [ + "share_id" + ], + "isUnique": false + }, + "share_recipients_user_id_idx": { + "name": "share_recipients_user_id_idx", + "columns": [ + "recipient_user_id" + ], + "isUnique": false + }, + "share_recipients_email_idx": { + "name": "share_recipients_email_idx", + "columns": [ + "recipient_email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shares": { + "name": "shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "matter_id": { + "name": "matter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "download_limit": { + "name": "download_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "downloads": { + "name": "downloads", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "private": { + "name": "private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "shares_token_unique": { + "name": "shares_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "shares_creator_status_created_idx": { + "name": "shares_creator_status_created_idx", + "columns": [ + "creator_id", + "status", + "created_at", + "id" + ], + "isUnique": false + }, + "shares_creator_private_created_idx": { + "name": "shares_creator_private_created_idx", + "columns": [ + "creator_id", + "private", + "created_at" + ], + "isUnique": false + }, + "shares_created_idx": { + "name": "shares_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "site_invitations": { + "name": "site_invitations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "site_invitations_token_unique": { + "name": "site_invitations_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "site_invitations_email_idx": { + "name": "site_invitations_email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "site_invitations_created_idx": { + "name": "site_invitations_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "site_invitations_expires_idx": { + "name": "site_invitations_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stats_rollups_hourly": { + "name": "stats_rollups_hourly", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bucket_start": { + "name": "bucket_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "metric_key": { + "name": "metric_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dimension_key": { + "name": "dimension_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "dimension_value": { + "name": "dimension_value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "unique_count": { + "name": "unique_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "stats_rollups_hourly_bucket_metric_dim_uniq": { + "name": "stats_rollups_hourly_bucket_metric_dim_uniq", + "columns": [ + "bucket_start", + "org_id", + "metric_key", + "dimension_key", + "dimension_value" + ], + "isUnique": true + }, + "stats_rollups_hourly_metric_bucket_idx": { + "name": "stats_rollups_hourly_metric_bucket_idx", + "columns": [ + "metric_key", + "bucket_start" + ], + "isUnique": false + }, + "stats_rollups_hourly_dimension_bucket_idx": { + "name": "stats_rollups_hourly_dimension_bucket_idx", + "columns": [ + "metric_key", + "dimension_key", + "bucket_start" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "storage_usage_breakdowns": { + "name": "storage_usage_breakdowns", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "storage_usage_breakdowns_org_category_uniq": { + "name": "storage_usage_breakdowns_org_category_uniq", + "columns": [ + "org_id", + "category" + ], + "isUnique": true + }, + "storage_usage_breakdowns_org_idx": { + "name": "storage_usage_breakdowns_org_idx", + "columns": [ + "org_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "storage_usage_ledger": { + "name": "storage_usage_ledger", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delta_bytes": { + "name": "delta_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "storage_usage_ledger_event_key_unique": { + "name": "storage_usage_ledger_event_key_unique", + "columns": [ + "event_key" + ], + "isUnique": true + }, + "storage_usage_ledger_occurred_idx": { + "name": "storage_usage_ledger_occurred_idx", + "columns": [ + "occurred_at" + ], + "isUnique": false + }, + "storage_usage_ledger_org_occurred_idx": { + "name": "storage_usage_ledger_org_occurred_idx", + "columns": [ + "org_id", + "occurred_at" + ], + "isUnique": false + }, + "storage_usage_ledger_storage_occurred_idx": { + "name": "storage_usage_ledger_storage_occurred_idx", + "columns": [ + "storage_id", + "occurred_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "storages": { + "name": "storages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "access_key": { + "name": "access_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secret_key": { + "name": "secret_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "custom_host": { + "name": "custom_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "egress_credit_billing_enabled": { + "name": "egress_credit_billing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "egress_credit_unit_bytes": { + "name": "egress_credit_unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 104857600 + }, + "egress_credit_per_unit": { + "name": "egress_credit_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "force_path_style": { + "name": "force_path_style", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_checked_at": { + "name": "status_checked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "system_options": { + "name": "system_options", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team_invite_links": { + "name": "team_invite_links", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "team_invite_links_token_unique": { + "name": "team_invite_links_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webdav_dead_properties": { + "name": "webdav_dead_properties", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_path": { + "name": "resource_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webdav_dead_properties_resource_prop_uniq": { + "name": "webdav_dead_properties_resource_prop_uniq", + "columns": [ + "org_id", + "resource_path", + "namespace", + "name" + ], + "isUnique": true + }, + "webdav_dead_properties_resource_idx": { + "name": "webdav_dead_properties_resource_idx", + "columns": [ + "org_id", + "resource_path" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webdav_locks": { + "name": "webdav_locks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_path": { + "name": "resource_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "depth": { + "name": "depth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'infinity'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webdav_locks_token_unique": { + "name": "webdav_locks_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "webdav_locks_resource_idx": { + "name": "webdav_locks_resource_idx", + "columns": [ + "org_id", + "resource_path" + ], + "isUnique": false + }, + "webdav_locks_expires_idx": { + "name": "webdav_locks_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webhook_events": { + "name": "webhook_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cloud'" + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'order.quota_changed'" + }, + "payload_hash": { + "name": "payload_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_payload": { + "name": "raw_payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "processed_at": { + "name": "processed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webhook_events_source_event_uniq": { + "name": "webhook_events_source_event_uniq", + "columns": [ + "source", + "event_id" + ], + "isUnique": true + }, + "webhook_events_source_created_idx": { + "name": "webhook_events_source_created_idx", + "columns": [ + "source", + "created_at" + ], + "isUnique": false + }, + "webhook_events_status_idx": { + "name": "webhook_events_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "webhook_events_processed_idx": { + "name": "webhook_events_processed_idx", + "columns": [ + "processed_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apikey": { + "name": "apikey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_request": { + "name": "last_request", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + "reference_id" + ], + "isUnique": false + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + "key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deviceCode": { + "name": "deviceCode", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "deviceCode_device_code_idx": { + "name": "deviceCode_device_code_idx", + "columns": [ + "device_code" + ], + "isUnique": false + }, + "deviceCode_user_code_idx": { + "name": "deviceCode_user_code_idx", + "columns": [ + "user_code" + ], + "isUnique": false + }, + "deviceCode_status_idx": { + "name": "deviceCode_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "downloader_bootstrap_credentials": { + "name": "downloader_bootstrap_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "downloader_bootstrap_credentials_token_hash_unique": { + "name": "downloader_bootstrap_credentials_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "downloader_bootstrap_token_hash_idx": { + "name": "downloader_bootstrap_token_hash_idx", + "columns": [ + "token_hash" + ], + "isUnique": false + }, + "downloader_bootstrap_user_idx": { + "name": "downloader_bootstrap_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "downloader_bootstrap_consumed_idx": { + "name": "downloader_bootstrap_consumed_idx", + "columns": [ + "consumed_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "downloader_bootstrap_credentials_user_id_user_id_fk": { + "name": "downloader_bootstrap_credentials_user_id_user_id_fk", + "tableFrom": "downloader_bootstrap_credentials", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthAccessToken": { + "name": "oauthAccessToken", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauthAccessToken_token_unique": { + "name": "oauthAccessToken_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "oauthAccessToken_client_id_idx": { + "name": "oauthAccessToken_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauthAccessToken_session_id_idx": { + "name": "oauthAccessToken_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "oauthAccessToken_user_id_idx": { + "name": "oauthAccessToken_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "oauthAccessToken_refresh_id_idx": { + "name": "oauthAccessToken_refresh_id_idx", + "columns": [ + "refresh_id" + ], + "isUnique": false + }, + "oauthAccessToken_token_idx": { + "name": "oauthAccessToken_token_idx", + "columns": [ + "token" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauthAccessToken_client_id_oauthClient_client_id_fk": { + "name": "oauthAccessToken_client_id_oauthClient_client_id_fk", + "tableFrom": "oauthAccessToken", + "tableTo": "oauthClient", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauthAccessToken_session_id_session_id_fk": { + "name": "oauthAccessToken_session_id_session_id_fk", + "tableFrom": "oauthAccessToken", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauthAccessToken_user_id_user_id_fk": { + "name": "oauthAccessToken_user_id_user_id_fk", + "tableFrom": "oauthAccessToken", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauthAccessToken_refresh_id_oauthRefreshToken_id_fk": { + "name": "oauthAccessToken_refresh_id_oauthRefreshToken_id_fk", + "tableFrom": "oauthAccessToken", + "tableTo": "oauthRefreshToken", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthClient": { + "name": "oauthClient", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disabled": { + "name": "disabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contacts": { + "name": "contacts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public": { + "name": "public", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "oauthClient_client_id_unique": { + "name": "oauthClient_client_id_unique", + "columns": [ + "client_id" + ], + "isUnique": true + }, + "oauthClient_client_id_idx": { + "name": "oauthClient_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauthClient_user_id_idx": { + "name": "oauthClient_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauthClient_user_id_user_id_fk": { + "name": "oauthClient_user_id_user_id_fk", + "tableFrom": "oauthClient", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthConsent": { + "name": "oauthConsent", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "oauthConsent_client_id_idx": { + "name": "oauthConsent_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauthConsent_user_id_idx": { + "name": "oauthConsent_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauthConsent_client_id_oauthClient_client_id_fk": { + "name": "oauthConsent_client_id_oauthClient_client_id_fk", + "tableFrom": "oauthConsent", + "tableTo": "oauthClient", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauthConsent_user_id_user_id_fk": { + "name": "oauthConsent_user_id_user_id_fk", + "tableFrom": "oauthConsent", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauthRefreshToken": { + "name": "oauthRefreshToken", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "revoked": { + "name": "revoked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_time": { + "name": "auth_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauthRefreshToken_token_unique": { + "name": "oauthRefreshToken_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "oauthRefreshToken_client_id_idx": { + "name": "oauthRefreshToken_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauthRefreshToken_session_id_idx": { + "name": "oauthRefreshToken_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "oauthRefreshToken_user_id_idx": { + "name": "oauthRefreshToken_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "oauthRefreshToken_token_idx": { + "name": "oauthRefreshToken_token_idx", + "columns": [ + "token" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oauthRefreshToken_client_id_oauthClient_client_id_fk": { + "name": "oauthRefreshToken_client_id_oauthClient_client_id_fk", + "tableFrom": "oauthRefreshToken", + "tableTo": "oauthClient", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauthRefreshToken_session_id_session_id_fk": { + "name": "oauthRefreshToken_session_id_session_id_fk", + "tableFrom": "oauthRefreshToken", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauthRefreshToken_user_id_user_id_fk": { + "name": "oauthRefreshToken_user_id_user_id_fk", + "tableFrom": "oauthRefreshToken", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "session_created_idx": { + "name": "session_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned": { + "name": "banned", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_username_unique": { + "name": "user_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "user_created_idx": { + "name": "user_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "user_lastActiveAt_idx": { + "name": "user_lastActiveAt_idx", + "columns": [ + "last_active_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "matters_webdav_children_idx": { + "columns": { + "\"dirtype\" desc": { + "isExpression": true + } + } + }, + "matters_list_page_idx": { + "columns": { + "\"dirtype\" desc": { + "isExpression": true + } + } + } + } + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index d54cf148..1ba4e335 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -568,6 +568,13 @@ "when": 1785337905649, "tag": "0081_spotty_boomerang", "breakpoints": true + }, + { + "idx": 82, + "version": "6", + "when": 1785351402721, + "tag": "0082_agent_oauth_consent_last_used_at", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/adapters/repos/agent-oauth.test.ts b/server/adapters/repos/agent-oauth.test.ts index e125865a..d6d6d94f 100644 --- a/server/adapters/repos/agent-oauth.test.ts +++ b/server/adapters/repos/agent-oauth.test.ts @@ -124,6 +124,7 @@ describe('Agent OAuth gateway', () => { referenceId: orgId, scopes: JSON.stringify([AuthorizationScope.OBJECTS_READ]), createdAt: new Date('2026-07-29T12:00:00.000Z'), + lastUsedAt: new Date('2026-07-29T12:20:00.000Z'), updatedAt: new Date('2026-07-29T12:01:00.000Z'), }, { @@ -133,9 +134,32 @@ describe('Agent OAuth gateway', () => { referenceId: null, scopes: JSON.stringify([AuthorizationScope.OBJECTS_READ]), createdAt: new Date('2026-07-29T12:02:00.000Z'), + lastUsedAt: null, updatedAt: new Date('2026-07-29T12:03:00.000Z'), }, ]) + await db.insert(authSchema.oauthAccessToken).values([ + { + id: 'access-older', + token: 'hashed-access-older', + clientId: AGENT_OAUTH_CLIENT_ID, + userId, + referenceId: orgId, + expiresAt: new Date(Date.now() + 60_000), + createdAt: new Date('2026-07-29T12:05:00.000Z'), + scopes: JSON.stringify([AuthorizationScope.OBJECTS_READ]), + }, + { + id: 'access-newer', + token: 'hashed-access-newer', + clientId: AGENT_OAUTH_CLIENT_ID, + userId, + referenceId: orgId, + expiresAt: new Date(Date.now() + 60_000), + createdAt: new Date('2026-07-29T12:10:00.000Z'), + scopes: JSON.stringify([AuthorizationScope.OBJECTS_READ]), + }, + ]) await expect(createAgentOAuthGateway().listGrants(db, userId)).resolves.toEqual([ { @@ -145,11 +169,51 @@ describe('Agent OAuth gateway', () => { orgId, scopes: [AuthorizationScope.OBJECTS_READ], createdAt: '2026-07-29T12:00:00.000Z', - updatedAt: '2026-07-29T12:01:00.000Z', + lastUsedAt: '2026-07-29T12:20:00.000Z', }, ]) }) + it('records actual delegated grant use without treating token issuance as use', async () => { + const { db } = await createTestApp() + const gateway = createAgentOAuthGateway() + const userId = 'oauth-user' + const orgId = 'oauth-org' + await insertUserAndOrg(db, userId, orgId) + await db.insert(authSchema.oauthConsent).values({ + id: 'grant-1', + clientId: AGENT_OAUTH_CLIENT_ID, + userId, + referenceId: orgId, + scopes: JSON.stringify([AuthorizationScope.OBJECTS_READ]), + createdAt: new Date('2026-07-29T12:00:00.000Z'), + updatedAt: new Date('2026-07-29T12:01:00.000Z'), + }) + await db.insert(authSchema.oauthAccessToken).values({ + id: 'access-1', + token: 'hashed-access', + clientId: AGENT_OAUTH_CLIENT_ID, + userId, + referenceId: orgId, + expiresAt: new Date(Date.now() + 60_000), + createdAt: new Date('2026-07-29T12:10:00.000Z'), + scopes: JSON.stringify([AuthorizationScope.OBJECTS_READ]), + }) + + await expect(gateway.listGrants(db, userId)).resolves.toMatchObject([{ id: 'grant-1', lastUsedAt: null }]) + + await gateway.recordGrantUse(db, { + grantId: 'grant-1', + userId, + orgId, + now: new Date('2026-07-29T12:30:00.000Z'), + }) + + await expect(gateway.listGrants(db, userId)).resolves.toMatchObject([ + { id: 'grant-1', lastUsedAt: '2026-07-29T12:30:00.000Z' }, + ]) + }) + it('revokes only the managed client grant for the selected workspace', async () => { const { db } = await createTestApp() const userId = 'oauth-user' diff --git a/server/adapters/repos/agent-oauth.ts b/server/adapters/repos/agent-oauth.ts index 0815b88d..f3016619 100644 --- a/server/adapters/repos/agent-oauth.ts +++ b/server/adapters/repos/agent-oauth.ts @@ -125,7 +125,7 @@ export function createAgentOAuthGateway(): AgentOAuthGateway { orgId: oauthConsent.referenceId, scopes: oauthConsent.scopes, createdAt: oauthConsent.createdAt, - updatedAt: oauthConsent.updatedAt, + lastUsedAt: oauthConsent.lastUsedAt, }) .from(oauthConsent) .where(and(eq(oauthConsent.userId, userId), eq(oauthConsent.clientId, AGENT_OAUTH_CLIENT_ID))) @@ -139,12 +139,26 @@ export function createAgentOAuthGateway(): AgentOAuthGateway { orgId: row.orgId, scopes: parseScopes(row.scopes).filter(isAuthorizationScope), createdAt: toIso(row.createdAt), - updatedAt: toIso(row.updatedAt), + lastUsedAt: row.lastUsedAt ? toIso(row.lastUsedAt) : null, }, ] }) }, + async recordGrantUse(db, input) { + await db + .update(oauthConsent) + .set({ lastUsedAt: input.now }) + .where( + and( + eq(oauthConsent.id, input.grantId), + eq(oauthConsent.userId, input.userId), + eq(oauthConsent.referenceId, input.orgId), + eq(oauthConsent.clientId, AGENT_OAUTH_CLIENT_ID), + ), + ) + }, + async revokeGrant(db, input) { const grants = await db .select({ diff --git a/server/adapters/repos/org.ts b/server/adapters/repos/org.ts index 1f83d462..adeb2268 100644 --- a/server/adapters/repos/org.ts +++ b/server/adapters/repos/org.ts @@ -1,5 +1,5 @@ import { isPersonalOrgLike } from '@shared/org-slugs' -import { and, eq } from 'drizzle-orm' +import { and, eq, inArray } from 'drizzle-orm' import { member, organization } from '../../db/auth-schema' import type { Database } from '../../platform/interface' import type { OrgRepo } from '../../usecases/ports' @@ -29,6 +29,16 @@ export function createOrgRepo(db: Database): OrgRepo { return rows[0]?.role ?? null } + async function getOrgNames(orgIds: string[]): Promise> { + const unique = [...new Set(orgIds)].filter(Boolean) + if (unique.length === 0) return new Map() + const rows = await db + .select({ id: organization.id, name: organization.name }) + .from(organization) + .where(inArray(organization.id, unique)) + return new Map(rows.map((row) => [row.id, row.name])) + } + async function isPersonalOrg(orgId: string): Promise { const rows = await db .select({ slug: organization.slug, metadata: organization.metadata }) @@ -61,5 +71,5 @@ export function createOrgRepo(db: Database): OrgRepo { return orgId === (await findPersonalOrg(userId)) } - return { findPersonalOrg, getMemberRole, canReadOrg, canWriteToOrg, canManageAgentAccess, isPersonalOrg } + return { findPersonalOrg, getMemberRole, getOrgNames, canReadOrg, canWriteToOrg, canManageAgentAccess, isPersonalOrg } } diff --git a/server/app.ts b/server/app.ts index ca2a5634..afa5f57d 100644 --- a/server/app.ts +++ b/server/app.ts @@ -537,3 +537,4 @@ export type AdminOverviewRoute = typeof adminOverview export type AdminStatsRoute = typeof adminStats export type StorageUsageRoute = typeof storageUsage export type AgentApiKeysRoute = typeof agentApiKeys +export type AgentOAuthGrantsRoute = typeof agentOAuthGrants diff --git a/server/auth/agent-oauth-provider.test.ts b/server/auth/agent-oauth-provider.test.ts index 02abdbff..ca015058 100644 --- a/server/auth/agent-oauth-provider.test.ts +++ b/server/auth/agent-oauth-provider.test.ts @@ -12,6 +12,7 @@ function createGateway(): AgentOAuthGateway { assertLiveGrant: vi.fn(), verifyAccessToken: vi.fn(), listGrants: vi.fn(), + recordGrantUse: vi.fn(), revokeGrant: vi.fn(), } } diff --git a/server/db/auth-schema.test.ts b/server/db/auth-schema.test.ts index 258663a3..aadefa31 100644 --- a/server/db/auth-schema.test.ts +++ b/server/db/auth-schema.test.ts @@ -154,6 +154,8 @@ describe('Agent OAuth tables', () => { expect(oauthConsent.referenceId.name).toBe('reference_id') expect(oauthConsent.scopes.notNull).toBe(true) + expect(oauthConsent.lastUsedAt.name).toBe('last_used_at') + expect(oauthConsent.lastUsedAt.notNull).toBe(false) expect(foreignKeys).toHaveLength(2) expect(oauthConsent.updatedAt.onUpdateFn?.()).toBeInstanceOf(Date) expect(foreignKeys.map((foreignKey) => foreignKey.reference().foreignColumns[0].name)).toEqual(['client_id', 'id']) diff --git a/server/db/auth-schema.ts b/server/db/auth-schema.ts index 2a2ba97b..b37b80f4 100644 --- a/server/db/auth-schema.ts +++ b/server/db/auth-schema.ts @@ -327,6 +327,7 @@ export const oauthConsent = sqliteTable( createdAt: integer('created_at', { mode: 'timestamp_ms' }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .notNull(), + lastUsedAt: integer('last_used_at', { mode: 'timestamp_ms' }), updatedAt: integer('updated_at', { mode: 'timestamp_ms' }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .$onUpdate(() => /* @__PURE__ */ new Date()) diff --git a/server/http/agent-oauth-grants.integration.test.ts b/server/http/agent-oauth-grants.integration.test.ts index 7f3571ff..d67f43b9 100644 --- a/server/http/agent-oauth-grants.integration.test.ts +++ b/server/http/agent-oauth-grants.integration.test.ts @@ -1,7 +1,12 @@ import { createHash } from 'node:crypto' -import { AGENT_OAUTH_CLIENT_ID } from '@shared/agent-oauth' +import { + AGENT_OAUTH_ACCESS_TOKEN_SECONDS, + AGENT_OAUTH_CLIENT_ID, + AGENT_OAUTH_CLIENT_NAME, + AGENT_OAUTH_REFRESH_TOKEN_SECONDS, +} from '@shared/agent-oauth' import { AuthorizationScope } from '@shared/authorization' -import { sql } from 'drizzle-orm' +import { eq, sql } from 'drizzle-orm' import { describe, expect, it } from 'vitest' import * as authSchema from '../db/auth-schema.js' import { authedHeaders, createTestApp } from '../test/setup.js' @@ -71,6 +76,88 @@ async function insertGrant( } describe('Agent OAuth grants API integration', () => { + it('returns server-owned Agent OAuth consent context for the active workspace', async () => { + const { app, db } = await createTestApp() + const headers = await authedHeaders(app, 'agent-consent@example.com') + const { orgId } = await getUserAndPersonalOrg(db, 'agent-consent@example.com') + const oauthQuery = new URLSearchParams({ + client_id: AGENT_OAUTH_CLIENT_ID, + redirect_uri: 'http://127.0.0.1:8484/callback', + response_type: 'code', + scope: `${AuthorizationScope.OBJECTS_READ} ${AuthorizationScope.QUOTA_READ} openid offline_access`, + }).toString() + + const res = await app.request(`/api/agent-oauth-consent?oauthQuery=${encodeURIComponent(oauthQuery)}`, { headers }) + + expect(res.status).toBe(200) + await expect(res.json()).resolves.toEqual({ + clientId: AGENT_OAUTH_CLIENT_ID, + clientName: AGENT_OAUTH_CLIENT_NAME, + instanceOrigin: 'http://localhost', + workspace: { id: orgId, name: expect.any(String) }, + scopes: [AuthorizationScope.OBJECTS_READ, AuthorizationScope.QUOTA_READ], + standardScopes: ['openid', 'offline_access'], + redirectUri: 'http://127.0.0.1:8484/callback', + grantLifetime: { + accessTokenSeconds: AGENT_OAUTH_ACCESS_TOKEN_SECONDS, + refreshTokenSeconds: AGENT_OAUTH_REFRESH_TOKEN_SECONDS, + }, + }) + }) + + it('revalidates OAuth consent submission through the Agent Access API', async () => { + const { app } = await createTestApp() + const headers = await authedHeaders(app, 'agent-submit@example.com') + + const res = await app.request('/api/agent-oauth-consent', { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/json' }, + body: JSON.stringify({ accept: true, oauthQuery: 'client_id=zpan-agent&response_type=token' }), + }) + + expect(res.status).toBe(400) + await expect(res.json()).resolves.toMatchObject({ + error: { + message: 'Invalid Agent OAuth request', + }, + }) + }) + + it('submits full OAuth consent through the Agent Access API', async () => { + const { app } = await createTestApp() + const headers = await authedHeaders(app, 'agent-submit-success@example.com') + const oauthParams = new URLSearchParams({ + client_id: AGENT_OAUTH_CLIENT_ID, + redirect_uri: 'http://127.0.0.1:8484/callback', + response_type: 'code', + scope: `${AuthorizationScope.OBJECTS_READ} ${AuthorizationScope.QUOTA_READ} openid offline_access`, + state: 'agent-submit-success', + code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM', + code_challenge_method: 'S256', + }) + const authorize = await app.request(`/api/auth/oauth2/authorize?${oauthParams}`, { + headers: { ...headers, Origin: 'http://localhost' }, + }) + const consentLocation = authorize.headers.get('location') + expect(authorize.status).toBe(302) + expect(consentLocation).toMatch(/^\/settings\/agent-access\?/) + + const consent = await app.request('/api/agent-oauth-consent', { + method: 'POST', + headers: { ...headers, Origin: 'http://localhost', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + accept: true, + oauthQuery: consentLocation?.slice(consentLocation.indexOf('?') + 1), + }), + }) + const consentBody = await consent.text() + + expect(consent.status, consentBody).toBe(200) + expect(JSON.parse(consentBody)).toMatchObject({ + url: expect.stringMatching(/^http:\/\/127\.0\.0\.1:8484\/callback\?code=/), + }) + }) + it('lists and revokes the current user grant family', async () => { const { app, db } = await createTestApp() const headers = await authedHeaders(app, 'agent-grants@example.com') @@ -84,11 +171,14 @@ describe('Agent OAuth grants API integration', () => { { id: 'grant-1', clientId: AGENT_OAUTH_CLIENT_ID, + clientName: 'ZPan Agent', userId, orgId, + workspaceName: expect.any(String), scopes: [AuthorizationScope.OBJECTS_READ, AuthorizationScope.QUOTA_READ], createdAt: '2026-07-29T12:00:00.000Z', - updatedAt: '2026-07-29T12:00:00.000Z', + lastUsedAt: null, + status: 'active', }, ], }) @@ -108,9 +198,18 @@ describe('Agent OAuth grants API integration', () => { await insertTeamOrg(db, 'other-workspace', userId) await insertGrant(db, { userId, orgId, scopes: [AuthorizationScope.OBJECTS_READ] }) + const list = await app.request('/api/agent-oauth-grants', { headers }) + expect(list.status).toBe(200) + await expect(list.json()).resolves.toMatchObject({ items: [{ id: 'grant-1', lastUsedAt: null }] }) + const bearer = { Authorization: 'Bearer live-agent-token' } const allowed = await app.request('/api/objects', { headers: bearer }) expect(allowed.status).toBe(200) + const [usedGrant] = await db + .select({ lastUsedAt: authSchema.oauthConsent.lastUsedAt }) + .from(authSchema.oauthConsent) + .where(eq(authSchema.oauthConsent.id, 'grant-1')) + expect(usedGrant.lastUsedAt).toBeInstanceOf(Date) const wrongWorkspace = await app.request('/api/objects?orgId=other-workspace', { headers: bearer }) expect(wrongWorkspace.status).toBe(403) diff --git a/server/http/agent-oauth-grants.ts b/server/http/agent-oauth-grants.ts index cde29087..88c5c721 100644 --- a/server/http/agent-oauth-grants.ts +++ b/server/http/agent-oauth-grants.ts @@ -1,22 +1,54 @@ import { OpenAPIHono, z } from '@hono/zod-openapi' -import { AuthorizationScope } from '@shared/authorization' +import { + agentOAuthConsentContextSchema, + agentOAuthConsentResultSchema, + agentOAuthConsentSubmitSchema, + agentOAuthGrantListSchema, +} from '@shared/schemas' import { requireAuth } from '../middleware/auth' import type { Env } from '../middleware/platform' +import { getAgentOAuthConsentContext } from '../usecases/agent-oauth-consent' import { listAgentOAuthGrants, revokeAgentOAuthGrant } from '../usecases/agent-oauth-grants' -import { authRoute, errorResponse, jsonContent } from './openapi' +import { authRoute, errorResponse, jsonBody, jsonContent } from './openapi' -const agentOAuthGrantSchema = z.object({ - id: z.string(), - clientId: z.string(), - userId: z.string(), - orgId: z.string(), - scopes: z.array(z.enum(Object.values(AuthorizationScope) as [AuthorizationScope, ...AuthorizationScope[]])), - createdAt: z.string(), - updatedAt: z.string(), -}) - -const listSchema = z.object({ items: z.array(agentOAuthGrantSchema) }) const paramsSchema = z.object({ grantId: z.string().min(1) }) +const consentContextQuerySchema = z.object({ oauthQuery: z.string().min(1) }) + +const consentContextRoute = authRoute( + { access: 'session' }, + { + operationId: 'getAgentOAuthConsentContext', + summary: 'Get pending Agent OAuth consent context', + tags: ['Agent Access'], + method: 'get', + path: '/agent-oauth-consent', + middleware: [requireAuth] as const, + request: { query: consentContextQuerySchema }, + responses: { + 200: jsonContent(agentOAuthConsentContextSchema, 'Agent OAuth consent context'), + 400: errorResponse('Invalid OAuth request'), + 403: errorResponse('Workspace access is required'), + }, + }, +) + +const consentSubmitRoute = authRoute( + { access: 'session' }, + { + operationId: 'submitAgentOAuthConsent', + summary: 'Submit Agent OAuth consent decision', + tags: ['Agent Access'], + method: 'post', + path: '/agent-oauth-consent', + middleware: [requireAuth] as const, + request: jsonBody(agentOAuthConsentSubmitSchema), + responses: { + 200: jsonContent(agentOAuthConsentResultSchema, 'Agent OAuth consent result'), + 400: errorResponse('Invalid OAuth request'), + 403: errorResponse('Workspace access is required'), + }, + }, +) const listRoute = authRoute( { access: 'session' }, @@ -28,7 +60,7 @@ const listRoute = authRoute( path: '/agent-oauth-grants', middleware: [requireAuth] as const, responses: { - 200: jsonContent(listSchema, 'Agent OAuth grants'), + 200: jsonContent(agentOAuthGrantListSchema, 'Agent OAuth grants'), }, }, ) @@ -51,6 +83,38 @@ const revokeRoute = authRoute( ) export const agentOAuthGrants = new OpenAPIHono() + .openapi(consentContextRoute, async (c) => { + const { oauthQuery } = c.req.valid('query') + const context = await getAgentOAuthConsentContext(c.get('deps'), { + userId: c.get('userId')!, + orgId: c.get('orgId'), + requestUrl: c.req.url, + oauthQuery, + }) + return c.json(context, 200) + }) + .openapi(consentSubmitRoute, async (c) => { + const { accept, oauthQuery } = c.req.valid('json') + await getAgentOAuthConsentContext(c.get('deps'), { + userId: c.get('userId')!, + orgId: c.get('orgId'), + requestUrl: c.req.url, + oauthQuery, + }) + const headers = new Headers(c.req.raw.headers) + headers.set('content-type', 'application/json') + headers.delete('content-length') + const response = await c.get('auth').handler( + new Request(new URL('/api/auth/oauth2/consent', c.req.url), { + method: 'POST', + headers, + body: JSON.stringify({ accept, oauth_query: oauthQuery }), + }), + ) + 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) + }) .openapi(listRoute, async (c) => { const result = await listAgentOAuthGrants(c.get('deps'), c.get('platform').db, { userId: c.get('userId')! }) return c.json(result, 200) diff --git a/server/http/auth/auth.cf-test.ts b/server/http/auth/auth.cf-test.ts index 5403160f..398f98b5 100644 --- a/server/http/auth/auth.cf-test.ts +++ b/server/http/auth/auth.cf-test.ts @@ -71,12 +71,12 @@ describe('[CF] Auth API', () => { expect(authorize.status).toBe(302) expect(consentLocation).toMatch(/^\/settings\/agent-access\?/) - const consent = await app.request('/api/auth/oauth2/consent', { + const consent = await app.request('/api/agent-oauth-consent', { method: 'POST', headers: { Cookie: cookie, Origin: 'http://localhost', 'Content-Type': 'application/json' }, body: JSON.stringify({ accept: true, - oauth_query: consentLocation?.slice(consentLocation.indexOf('?') + 1), + oauthQuery: consentLocation?.slice(consentLocation.indexOf('?') + 1), }), }) const consentBody = await consent.text() diff --git a/server/middleware/authz.test.ts b/server/middleware/authz.test.ts new file mode 100644 index 00000000..19e4b4c8 --- /dev/null +++ b/server/middleware/authz.test.ts @@ -0,0 +1,87 @@ +import { AuthorizationScope } from '@shared/authorization' +import { Hono } from 'hono' +import { describe, expect, it, vi } from 'vitest' +import { authorize, type RouteAuthorizationDeclaration } from './authz' +import type { AuthzContext, Env } from './platform' + +function probeApp(context: AuthzContext, declaration: RouteAuthorizationDeclaration) { + const recordGrantUse = vi.fn(async () => {}) + const app = new Hono() + app.use('/probe', async (c, next) => { + c.set('authzContext', context) + c.set('platform', { db: { kind: 'unit-db' } } as unknown as Env['Variables']['platform']) + c.set('deps', { + agentOAuth: { recordGrantUse }, + audit: { record: vi.fn() }, + org: { + getMemberRole: vi.fn(async () => 'owner'), + findPersonalOrg: vi.fn(async () => context.orgId), + }, + } as unknown as Env['Variables']['deps']) + await next() + }) + app.get('/probe', authorize(declaration), (c) => c.json({ ok: true })) + return { app, recordGrantUse } +} + +describe('authorize Agent OAuth grant-use tracking', () => { + const context: AuthzContext = { + credential: 'agent_oauth', + userId: 'user-1', + orgId: 'org-1', + fixedOrgId: 'org-1', + grantedScopes: new Set([AuthorizationScope.OBJECTS_READ]), + actor: { type: 'agent_oauth', ref: 'grant-1' }, + state: { clientId: 'zpan-agent' }, + } + + it('records actual Agent OAuth use for scoped protected routes', async () => { + const { app, recordGrantUse } = probeApp(context, { + access: 'protected', + scopes: [AuthorizationScope.OBJECTS_READ], + }) + + const res = await app.request('/probe') + + expect(res.status).toBe(200) + expect(recordGrantUse).toHaveBeenCalledTimes(1) + expect(recordGrantUse).toHaveBeenCalledWith( + { kind: 'unit-db' }, + expect.objectContaining({ + grantId: 'grant-1', + userId: 'user-1', + orgId: 'org-1', + now: expect.any(Date), + }), + ) + }) + + it('does not record display-only unscoped protected access as grant use', async () => { + const { app, recordGrantUse } = probeApp(context, { access: 'protected' }) + + const res = await app.request('/probe') + + expect(res.status).toBe(200) + expect(recordGrantUse).not.toHaveBeenCalled() + }) + + it('does not record non-Agent OAuth protected access as grant use', async () => { + const { app, recordGrantUse } = probeApp( + { + credential: 'session', + userId: 'user-1', + orgId: 'org-1', + fixedOrgId: null, + grantedScopes: null, + actor: { type: 'user', ref: 'user-1' }, + state: { firstParty: true }, + }, + { access: 'protected', scopes: [AuthorizationScope.OBJECTS_READ] }, + ) + + const res = await app.request('/probe') + + expect(res.status).toBe(200) + expect(recordGrantUse).not.toHaveBeenCalled() + }) +}) diff --git a/server/middleware/authz.ts b/server/middleware/authz.ts index 66f14b8c..c42b2ff2 100644 --- a/server/middleware/authz.ts +++ b/server/middleware/authz.ts @@ -108,6 +108,7 @@ export function authorize(declaration: RouteAuthorizationDeclaration) { }) if (decision.allowed) { if (decision.effectiveOrgId) c.set('orgId', decision.effectiveOrgId) + await recordAgentOAuthGrantUse(c, declaration, decision.effectiveOrgId) await next() return } @@ -135,6 +136,23 @@ export function requirePermission( }) } +async function recordAgentOAuthGrantUse( + c: Context, + declaration: RouteAuthorizationDeclaration, + effectiveOrgId: string | null, +) { + const context = c.get('authzContext') + if (declaration.access !== 'protected' || !declaration.scopes?.length) return + if (context.credential !== 'agent_oauth') return + if (!context.userId || !effectiveOrgId || context.actor?.type !== 'agent_oauth') return + await c.get('deps').agentOAuth.recordGrantUse(c.get('platform').db, { + grantId: context.actor.ref, + userId: context.userId, + orgId: effectiveOrgId, + now: new Date(), + }) +} + function deny( context: AuthzContext, status: 401 | 403, diff --git a/server/test/setup.ts b/server/test/setup.ts index 42171b29..12eec060 100644 --- a/server/test/setup.ts +++ b/server/test/setup.ts @@ -189,6 +189,7 @@ const AUTH_SCHEMA_SQL = ` reference_id TEXT, scopes TEXT NOT NULL, created_at INTEGER NOT NULL DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)), + last_used_at INTEGER, updated_at INTEGER NOT NULL DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) ); CREATE INDEX IF NOT EXISTS oauthConsent_client_id_idx ON oauthConsent(client_id); diff --git a/server/usecases/agent-oauth-consent.test.ts b/server/usecases/agent-oauth-consent.test.ts new file mode 100644 index 00000000..bc1211c2 --- /dev/null +++ b/server/usecases/agent-oauth-consent.test.ts @@ -0,0 +1,126 @@ +import { AGENT_OAUTH_CLIENT_ID, AGENT_OAUTH_CLIENT_NAME } from '@shared/agent-oauth' +import { AuthorizationScope } from '@shared/authorization' +import { describe, expect, it, vi } from 'vitest' +import { getAgentOAuthConsentContext } from './agent-oauth-consent' +import type { OrgRepo } from './ports' + +function org(overrides: Partial = {}): OrgRepo { + return { + findPersonalOrg: vi.fn(), + getMemberRole: vi.fn(), + getOrgNames: vi.fn(async () => new Map([['org-1', 'Personal']])), + canReadOrg: vi.fn(async () => true), + canWriteToOrg: vi.fn(), + canManageAgentAccess: vi.fn(), + isPersonalOrg: vi.fn(), + ...overrides, + } +} + +function oauthQuery(overrides: Record = {}) { + return new URLSearchParams({ + client_id: AGENT_OAUTH_CLIENT_ID, + redirect_uri: 'http://127.0.0.1:8484/callback', + response_type: 'code', + scope: `openid offline_access ${AuthorizationScope.OBJECTS_READ} ${AuthorizationScope.QUOTA_READ}`, + ...overrides, + }).toString() +} + +describe('Agent OAuth consent usecase', () => { + it('builds server-owned consent context for the active workspace', async () => { + await expect( + getAgentOAuthConsentContext( + { org: org() }, + { + userId: 'user-1', + orgId: 'org-1', + requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + oauthQuery: oauthQuery(), + }, + ), + ).resolves.toEqual({ + clientId: AGENT_OAUTH_CLIENT_ID, + clientName: AGENT_OAUTH_CLIENT_NAME, + instanceOrigin: 'https://zpan.example.test', + workspace: { id: 'org-1', name: 'Personal' }, + scopes: [AuthorizationScope.OBJECTS_READ, AuthorizationScope.QUOTA_READ], + standardScopes: ['openid', 'offline_access'], + redirectUri: 'http://127.0.0.1:8484/callback', + grantLifetime: { + accessTokenSeconds: 900, + refreshTokenSeconds: 2_592_000, + }, + }) + }) + + it('keeps the active workspace id when the workspace name is unavailable', async () => { + await expect( + getAgentOAuthConsentContext( + { org: org({ getOrgNames: vi.fn(async () => new Map()) }) }, + { + userId: 'user-1', + orgId: 'org-1', + requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + oauthQuery: oauthQuery(), + }, + ), + ).resolves.toMatchObject({ + workspace: { id: 'org-1', name: null }, + }) + }) + + it('rejects requests that are not the managed authorization-code client flow', async () => { + await expect( + getAgentOAuthConsentContext( + { org: org() }, + { + userId: 'user-1', + orgId: 'org-1', + requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + oauthQuery: oauthQuery({ response_type: 'token' }), + }, + ), + ).rejects.toMatchObject({ httpStatus: 400 }) + }) + + it('rejects untrusted redirect URIs and non-grantable scopes', async () => { + await expect( + getAgentOAuthConsentContext( + { org: org() }, + { + userId: 'user-1', + orgId: 'org-1', + requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + oauthQuery: oauthQuery({ redirect_uri: 'https://evil.example/callback' }), + }, + ), + ).rejects.toMatchObject({ httpStatus: 400 }) + + await expect( + getAgentOAuthConsentContext( + { org: org() }, + { + userId: 'user-1', + orgId: 'org-1', + requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + oauthQuery: oauthQuery({ scope: 'objects:purge' }), + }, + ), + ).rejects.toMatchObject({ httpStatus: 400 }) + }) + + it('rejects missing or inaccessible workspaces', async () => { + await expect( + getAgentOAuthConsentContext( + { org: org({ canReadOrg: vi.fn(async () => false) }) }, + { + userId: 'user-1', + orgId: 'org-1', + requestUrl: 'https://zpan.example.test/api/agent-oauth-consent', + oauthQuery: oauthQuery(), + }, + ), + ).rejects.toMatchObject({ httpStatus: 403 }) + }) +}) diff --git a/server/usecases/agent-oauth-consent.ts b/server/usecases/agent-oauth-consent.ts new file mode 100644 index 00000000..20960e54 --- /dev/null +++ b/server/usecases/agent-oauth-consent.ts @@ -0,0 +1,63 @@ +import { + AGENT_OAUTH_ACCESS_TOKEN_SECONDS, + AGENT_OAUTH_CLIENT_ID, + AGENT_OAUTH_CLIENT_NAME, + AGENT_OAUTH_REFRESH_TOKEN_SECONDS, + AGENT_OAUTH_STANDARD_SCOPES, + RESTISH_OAUTH_REDIRECT_URIS, +} from '@shared/agent-oauth' +import { isAuthorizationScope } from '@shared/authorization' +import { type AgentGrantableScope, type AgentOAuthConsentContext, agentGrantableScopeSchema } from '@shared/schemas' +import type { Deps } from './deps' +import { badRequest, forbidden } from './ports' + +export async function getAgentOAuthConsentContext( + deps: Pick, + input: { userId: string; orgId: string | null; requestUrl: string; oauthQuery: string }, +): Promise { + const params = new URLSearchParams(input.oauthQuery) + const clientId = params.get('client_id') + const redirectUri = params.get('redirect_uri') + const responseType = params.get('response_type') + const scopeValue = params.get('scope') ?? '' + + if (clientId !== AGENT_OAUTH_CLIENT_ID || responseType !== 'code' || !redirectUri) { + throw badRequest('Invalid Agent OAuth request') + } + if (!RESTISH_OAUTH_REDIRECT_URIS.includes(redirectUri as (typeof RESTISH_OAUTH_REDIRECT_URIS)[number])) { + throw badRequest('Invalid Agent 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 scopes = requestedScopes.filter(isAgentGrantableScope) + if (scopes.length === 0 || requestedScopes.length !== standardScopes.length + scopes.length) { + throw badRequest('Invalid Agent OAuth scope') + } + + const orgId = input.orgId + if (!orgId || !(await deps.org.canReadOrg(input.userId, orgId))) { + throw forbidden('Workspace access is required for Agent OAuth') + } + const names = await deps.org.getOrgNames([orgId]) + + return { + clientId, + clientName: AGENT_OAUTH_CLIENT_NAME, + instanceOrigin: new URL(input.requestUrl).origin, + workspace: { id: orgId, name: names.get(orgId) ?? null }, + scopes, + standardScopes, + redirectUri, + grantLifetime: { + accessTokenSeconds: AGENT_OAUTH_ACCESS_TOKEN_SECONDS, + refreshTokenSeconds: AGENT_OAUTH_REFRESH_TOKEN_SECONDS, + }, + } +} + +function isAgentGrantableScope(scope: string): scope is AgentGrantableScope { + return isAuthorizationScope(scope) && agentGrantableScopeSchema.safeParse(scope).success +} diff --git a/server/usecases/agent-oauth-grants.test.ts b/server/usecases/agent-oauth-grants.test.ts index e3061ef2..d192338a 100644 --- a/server/usecases/agent-oauth-grants.test.ts +++ b/server/usecases/agent-oauth-grants.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { listAgentOAuthGrants, revokeAgentOAuthGrant } from './agent-oauth-grants' -import type { AgentOAuthGateway } from './ports' +import type { AgentOAuthGateway, OrgRepo } from './ports' const db = {} as never @@ -10,11 +10,25 @@ function gateway(overrides: Partial = {}): AgentOAuthGateway assertLiveGrant: vi.fn(), verifyAccessToken: vi.fn(), listGrants: vi.fn(async () => []), + recordGrantUse: vi.fn(), revokeGrant: vi.fn(async () => true), ...overrides, } } +function org(overrides: Partial = {}): OrgRepo { + return { + findPersonalOrg: vi.fn(), + getMemberRole: vi.fn(), + 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', () => { it('lists grants through the gateway', async () => { const agentOAuth = gateway({ @@ -26,21 +40,24 @@ describe('Agent OAuth grant usecases', () => { orgId: 'org-1', scopes: [], createdAt: '2026-07-29T12:00:00.000Z', - updatedAt: '2026-07-29T12:00:00.000Z', + lastUsedAt: null, }, ]), }) - await expect(listAgentOAuthGrants({ agentOAuth }, db, { userId: 'user-1' })).resolves.toEqual({ + await expect(listAgentOAuthGrants({ agentOAuth, org: org() }, db, { userId: 'user-1' })).resolves.toEqual({ items: [ { id: 'grant-1', clientId: 'zpan-agent', + clientName: 'ZPan Agent', userId: 'user-1', orgId: 'org-1', + workspaceName: 'Personal', scopes: [], createdAt: '2026-07-29T12:00:00.000Z', - updatedAt: '2026-07-29T12:00:00.000Z', + lastUsedAt: null, + status: 'active', }, ], }) diff --git a/server/usecases/agent-oauth-grants.ts b/server/usecases/agent-oauth-grants.ts index c8171364..0489966e 100644 --- a/server/usecases/agent-oauth-grants.ts +++ b/server/usecases/agent-oauth-grants.ts @@ -1,14 +1,33 @@ +import { + type AgentGrantableScope, + type AgentOAuthGrant as AgentOAuthGrantDTO, + agentGrantableScopeSchema, + agentOAuthGrantDTO, +} from '@shared/schemas' import type { Database } from '../platform/interface' import type { Deps } from './deps' -import type { AgentOAuthGrant } from './ports' import { notFound } from './ports' export async function listAgentOAuthGrants( - deps: Pick, + deps: Pick, db: Database, input: { userId: string }, -): Promise<{ items: AgentOAuthGrant[] }> { - return { items: await deps.agentOAuth.listGrants(db, input.userId) } +): Promise<{ items: AgentOAuthGrantDTO[] }> { + const items = await deps.agentOAuth.listGrants(db, input.userId) + const orgNames = await deps.org.getOrgNames(items.map((item) => item.orgId)) + return { + items: items.map((item) => + agentOAuthGrantDTO({ + ...item, + scopes: item.scopes.filter(isAgentGrantableScope), + workspaceName: orgNames.get(item.orgId) ?? null, + }), + ), + } +} + +function isAgentGrantableScope(scope: string): scope is AgentGrantableScope { + return agentGrantableScopeSchema.safeParse(scope).success } export async function revokeAgentOAuthGrant( diff --git a/server/usecases/ports/agent-oauth.ts b/server/usecases/ports/agent-oauth.ts index 9aa309f5..02a0419d 100644 --- a/server/usecases/ports/agent-oauth.ts +++ b/server/usecases/ports/agent-oauth.ts @@ -16,7 +16,7 @@ export interface AgentOAuthGrant { orgId: string scopes: AuthorizationScope[] createdAt: string - updatedAt: string + lastUsedAt: string | null } export interface AgentOAuthGateway { @@ -27,5 +27,6 @@ export interface AgentOAuthGateway { ): Promise verifyAccessToken(db: Database, token: string): Promise listGrants(db: Database, userId: string): Promise + recordGrantUse(db: Database, input: { grantId: string; userId: string; orgId: string; now: Date }): 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 2f00efb2..9978e9b5 100644 --- a/server/usecases/ports/org.ts +++ b/server/usecases/ports/org.ts @@ -1,6 +1,7 @@ export interface OrgRepo { findPersonalOrg(userId: string): Promise getMemberRole(orgId: string, userId: string): Promise + getOrgNames(orgIds: string[]): Promise> canReadOrg(userId: string, orgId: string): Promise canWriteToOrg(userId: string, orgId: string): Promise canManageAgentAccess(userId: string, orgId: string): Promise diff --git a/server/usecases/team.test.ts b/server/usecases/team.test.ts index f495c5b9..fe7ec4e5 100644 --- a/server/usecases/team.test.ts +++ b/server/usecases/team.test.ts @@ -87,6 +87,7 @@ function makeDeps( org: { findPersonalOrg: async () => null, getMemberRole: async () => null, + getOrgNames: async () => new Map(), canReadOrg: async () => false, canWriteToOrg: async () => false, canManageAgentAccess: async () => false, diff --git a/shared/schemas/agent-oauth-grants.ts b/shared/schemas/agent-oauth-grants.ts new file mode 100644 index 00000000..7339d3bf --- /dev/null +++ b/shared/schemas/agent-oauth-grants.ts @@ -0,0 +1,69 @@ +import { z } from 'zod' +import { + AGENT_OAUTH_ACCESS_TOKEN_SECONDS, + AGENT_OAUTH_CLIENT_NAME, + AGENT_OAUTH_REFRESH_TOKEN_SECONDS, +} from '../agent-oauth' +import { agentGrantableScopeSchema } from './agent-api-keys' + +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().default(AGENT_OAUTH_CLIENT_NAME), + userId: z.string(), + orgId: z.string(), + workspaceName: z.string().nullable(), + scopes: z.array(agentGrantableScopeSchema), + 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(agentGrantableScopeSchema), + 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, + clientName: AGENT_OAUTH_CLIENT_NAME, + status: 'active', + } +} diff --git a/shared/schemas/index.ts b/shared/schemas/index.ts index 3a7f9798..8ed7bcb6 100644 --- a/shared/schemas/index.ts +++ b/shared/schemas/index.ts @@ -31,6 +31,25 @@ export { agentGrantableScopeSchema, agentScopeLabels, } from './agent-api-keys' +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, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 0c8fe744..1beca1ec 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1250,6 +1250,29 @@ "settings.agentAccess.revokeSuccess": "Agent API key revoked", "settings.agentAccess.revealedTitle": "Save Your Agent API Key", "settings.agentAccess.revealedWarning": "This is the only time this key will be shown. Store it securely.", + "settings.agentAccess.oauthConsentEyebrow": "Delegated OAuth access", + "settings.agentAccess.oauthConsentTitle": "Authorize ZPan Agent", + "settings.agentAccess.oauthConsentDescription": "Review the exact workspace and scopes Restish 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 Restish 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 Restish 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 Restish sessions for this workspace will stop immediately.", + "settings.agentAccess.oauthGrantRevokeSuccess": "OAuth grant revoked", "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 9b054d2b..5c2e719a 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1250,6 +1250,29 @@ "settings.agentAccess.revokeSuccess": "Agent API Key 已撤销", "settings.agentAccess.revealedTitle": "保存你的 Agent API Key", "settings.agentAccess.revealedWarning": "该 Key 只会显示一次,请妥善保存。", + "settings.agentAccess.oauthConsentEyebrow": "委托 OAuth 访问", + "settings.agentAccess.oauthConsentTitle": "授权 ZPan Agent", + "settings.agentAccess.oauthConsentDescription": "继续前请确认 Restish 将获得的具体工作空间和权限。", + "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": "请从 Restish 重新发起连接,生成新的授权请求。", + "settings.agentAccess.oauthConsentFailed": "无法完成 OAuth 授权。", + "settings.agentAccess.oauthWorkspaceFailed": "无法切换工作空间。", + "settings.agentAccess.oauthGrantsSection": "委托 OAuth 授权", + "settings.agentAccess.oauthGrantsDescription": "管理连接到你工作空间的 Restish OAuth 授权。", + "settings.agentAccess.oauthNoGrants": "暂无委托 OAuth 授权", + "settings.agentAccess.oauthGrantsError": "无法加载委托 OAuth 授权。", + "settings.agentAccess.oauthGrantRevokeTitle": "撤销 OAuth 授权", + "settings.agentAccess.oauthGrantRevokeConfirm": "撤销 {{client}} 对 {{workspace}} 的访问?该工作空间的 Restish 会话将立即停止。", + "settings.agentAccess.oauthGrantRevokeSuccess": "OAuth 授权已撤销", "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 e0c28cac..18246a83 100644 --- a/src/lib/api.test.ts +++ b/src/lib/api.test.ts @@ -48,6 +48,7 @@ import { getAdminDashboardStorageStats, getAdminDashboardTrafficStats, getAdminOverview, + getAgentOAuthConsentContext, getAnnouncement, getBackgroundJob, getChangelog, @@ -81,6 +82,7 @@ import { listAdminAnnouncements, listAdminAuditLogs, listAgentApiKeys, + listAgentOAuthGrants, listAnnouncements, listApiKeys, listAuthProviders, @@ -123,6 +125,7 @@ import { restoreObject, retryBackgroundJob, revokeAgentApiKey, + revokeAgentOAuthGrant, revokeIhostApiKey, revokeOrgEntitlement, revokeRemoteDownloadApiKey, @@ -139,6 +142,7 @@ import { sendDownloaderHeartbeat, serverEventsUrl, setSharePrivacy, + submitAgentOAuthConsent, testEmail, testImageDomainProvider, transferObject, @@ -3208,6 +3212,99 @@ describe('api', () => { }) }) + describe('Agent OAuth consent and grants', () => { + const sampleGrantList = { + items: [ + { + id: 'grant-1', + clientId: 'zpan-agent', + clientName: 'ZPan Agent', + userId: 'user-1', + orgId: 'org-1', + workspaceName: 'Personal', + scopes: ['objects:read'], + createdAt: '2026-07-29T00:00:00.000Z', + lastUsedAt: null, + status: 'active', + }, + ], + } + + it('loads server-owned OAuth consent context with the raw OAuth query', async () => { + const payload = { + clientId: 'zpan-agent', + clientName: 'ZPan Agent', + instanceOrigin: 'https://zpan.example.test', + workspace: { id: 'org-1', name: 'Personal' }, + scopes: ['objects:read'], + standardScopes: ['openid', 'offline_access'], + redirectUri: 'http://127.0.0.1:8484/callback', + grantLifetime: { accessTokenSeconds: 900, refreshTokenSeconds: 2_592_000 }, + } + vi.mocked(fetch).mockResolvedValueOnce(makeResponse(payload)) + + const result = await getAgentOAuthConsentContext('client_id=zpan-agent&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('oauthQuery=client_id%3Dzpan-agent%26scope%3Dobjects%253Aread') + expect(init.method).toBe('GET') + }) + + 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=zpan-agent' }) + + 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(init.method).toBe('POST') + expect(init.credentials).toBe('include') + expect(JSON.parse(init.body as string)).toEqual({ + accept: true, + oauthQuery: 'client_id=zpan-agent', + }) + }) + + it('throws an API error when OAuth consent submission is rejected with a non-JSON response', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 400, + statusText: 'Bad Request', + json: async () => { + throw new Error('not json') + }, + } as unknown as Response) + + await expect(submitAgentOAuthConsent({ accept: false, oauthQuery: 'client_id=zpan-agent' })).rejects.toThrow( + ApiError, + ) + }) + + it('lists delegated Agent OAuth grants', async () => { + vi.mocked(fetch).mockResolvedValueOnce(makeResponse(sampleGrantList)) + + const result = await listAgentOAuthGrants() + + expect(result).toEqual(sampleGrantList) + const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] + expect(url).toContain('/api/agent-oauth-grants') + expect(init.method).toBe('GET') + }) + + it('revokes delegated Agent OAuth grants with DELETE', async () => { + vi.mocked(fetch).mockResolvedValueOnce(makeResponse(null, true, 204)) + + await revokeAgentOAuthGrant('grant-1') + + const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] + expect(url).toContain('/api/agent-oauth-grants/grant-1') + expect(init.method).toBe('DELETE') + }) + }) + describe('listApiKeys', () => { const sampleKey = { id: 'key-1', diff --git a/src/lib/api.ts b/src/lib/api.ts index b9329f6d..804f2ce5 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -6,6 +6,11 @@ import type { AgentApiKeyCreateInput, AgentApiKeyList, AgentApiKeyRotateInput, + AgentOAuthConsentContext, + AgentOAuthConsentResult, + AgentOAuthConsentSubmit, + AgentOAuthGrant, + AgentOAuthGrantList, AllowedImageMime, AnnouncementInput, CloudCreditBalanceResponse, @@ -103,6 +108,7 @@ import { adminSiteInvitations, adminTeams, agentApiKeysApi, + agentOAuthGrantsApi, announcementsApi, authedSharesApi, authProviders, @@ -1092,7 +1098,17 @@ export function deleteIhostConfig() { // Agent Access API keys -export type { AgentApiKey, AgentApiKeyCreated, AgentApiKeyCreateInput, AgentApiKeyList, AgentApiKeyRotateInput } +export type { + AgentApiKey, + AgentApiKeyCreated, + AgentApiKeyCreateInput, + AgentApiKeyList, + AgentApiKeyRotateInput, + AgentOAuthConsentContext, + AgentOAuthConsentResult, + AgentOAuthGrant, + AgentOAuthGrantList, +} export function listAgentApiKeys(orgId: string, page = 1, pageSize = 50) { return unwrap( @@ -1124,6 +1140,22 @@ export function revokeAgentApiKey(orgId: string, keyId: string) { }) } +export function getAgentOAuthConsentContext(oauthQuery: string) { + return unwrap(agentOAuthGrantsApi['agent-oauth-consent'].$get({ query: { oauthQuery } })) +} + +export function submitAgentOAuthConsent(input: AgentOAuthConsentSubmit) { + return unwrap(agentOAuthGrantsApi['agent-oauth-consent'].$post({ json: input })) +} + +export function listAgentOAuthGrants() { + return unwrap(agentOAuthGrantsApi['agent-oauth-grants'].$get()) +} + +export function revokeAgentOAuthGrant(grantId: string) { + return discard(agentOAuthGrantsApi['agent-oauth-grants'][':grantId'].$delete({ param: { grantId } })) +} + // Image Host API Keys (via better-auth apiKey plugin) export interface IhostApiKey { diff --git a/src/lib/rpc.ts b/src/lib/rpc.ts index 089a6003..7624180a 100644 --- a/src/lib/rpc.ts +++ b/src/lib/rpc.ts @@ -7,6 +7,7 @@ import type { AdminStatsRoute, AdminTeamsRoute, AgentApiKeysRoute, + AgentOAuthGrantsRoute, AnnouncementsRoute, AuthedSharesRoute, AuthProvidersRoute, @@ -51,6 +52,7 @@ export const downloadTasksApi = hc('/api/downloads/tasks', o export const downloaderSelfApi = hc('/api/downloads/downloaders', opts) export const trash = hc('/api/trash', opts) export const agentApiKeysApi = hc('/api/workspaces', opts) +export const agentOAuthGrantsApi = 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/routes/_authenticated/settings/agent-access.test.tsx b/src/routes/_authenticated/settings/agent-access.test.tsx index 638170fd..5b013454 100644 --- a/src/routes/_authenticated/settings/agent-access.test.tsx +++ b/src/routes/_authenticated/settings/agent-access.test.tsx @@ -1,10 +1,20 @@ -import type { AgentApiKey } from '@shared/schemas' +import type { AgentApiKey, AgentOAuthGrant } from '@shared/schemas' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { toast } from 'sonner' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createAgentApiKey, listAgentApiKeys, revokeAgentApiKey, rotateAgentApiKey } from '@/lib/api' -import { useListOrganizations } from '@/lib/auth-client' +import { + createAgentApiKey, + getAgentOAuthConsentContext, + listAgentApiKeys, + listAgentOAuthGrants, + revokeAgentApiKey, + revokeAgentOAuthGrant, + rotateAgentApiKey, + submitAgentOAuthConsent, +} from '@/lib/api' +import { setActive, useListOrganizations } from '@/lib/auth-client' +import { redirectExternal } from '@/lib/browser-navigation' import { AgentAccessSettingsPage } from './agent-access' import { SettingsLayout } from './route' @@ -14,6 +24,7 @@ const state = vi.hoisted(() => ({ { id: 'org-2', name: 'Team Alpha' }, ], keys: [] as AgentApiKey[], + grants: [] as AgentOAuthGrant[], webdavEnabled: true, })) @@ -28,6 +39,20 @@ const translations: Record = { 'settings.agentAccess.scope.quotaRead': 'Quota: read workspace quota', 'settings.agentAccess.scope.storageUsageRead': 'Storage usage: read workspace usage', 'settings.agentAccess.managementRequired': 'Owner or admin access is required', + 'settings.agentAccess.oauthConsentTitle': 'Authorize ZPan Agent', + 'settings.agentAccess.oauthClient': 'Client', + 'settings.agentAccess.oauthOrigin': 'ZPan instance', + 'settings.agentAccess.oauthReturn': 'Return URL', + 'settings.agentAccess.oauthLifetime': 'Grant lifetime', + 'settings.agentAccess.oauthLifetimeValue': '30 days', + 'settings.agentAccess.oauthScopesTitle': 'Requested scopes', + 'settings.agentAccess.oauthApprove': 'Approve Access', + 'settings.agentAccess.oauthDeny': 'Deny', + 'settings.agentAccess.oauthExpiredTitle': 'OAuth request expired', + 'settings.agentAccess.oauthGrantsSection': 'Delegated OAuth Grants', + 'settings.agentAccess.oauthNoGrants': 'No delegated OAuth grants yet', + 'settings.agentAccess.oauthGrantRevokeTitle': 'Revoke OAuth Grant', + 'settings.agentAccess.oauthGrantRevokeSuccess': 'OAuth grant revoked', } vi.mock('react-i18next', () => ({ @@ -59,13 +84,22 @@ vi.mock('@/hooks/use-site-config', () => ({ vi.mock('@/lib/auth-client', () => ({ useListOrganizations: vi.fn(), + setActive: vi.fn(), +})) + +vi.mock('@/lib/browser-navigation', () => ({ + redirectExternal: vi.fn(), })) vi.mock('@/lib/api', () => ({ createAgentApiKey: vi.fn(), + getAgentOAuthConsentContext: vi.fn(), listAgentApiKeys: vi.fn(), + listAgentOAuthGrants: vi.fn(), revokeAgentApiKey: vi.fn(), + revokeAgentOAuthGrant: vi.fn(), rotateAgentApiKey: vi.fn(), + submitAgentOAuthConsent: vi.fn(), })) const queryClients: QueryClient[] = [] @@ -94,6 +128,7 @@ beforeEach(() => { disconnect() {} }, ) + Element.prototype.scrollIntoView = vi.fn() vi.mocked(useListOrganizations).mockReturnValue({ data: state.orgs } as never) vi.mocked(listAgentApiKeys).mockImplementation(async (orgId: string) => ({ items: state.keys.filter((item) => item.orgId === orgId), @@ -101,6 +136,9 @@ beforeEach(() => { page: 1, pageSize: 50, })) + vi.mocked(listAgentOAuthGrants).mockImplementation(async () => ({ items: state.grants })) + vi.mocked(setActive).mockResolvedValue({ data: null, error: null } as never) + window.history.replaceState(null, '', '/settings/agent-access') }) afterEach(() => { @@ -109,6 +147,7 @@ afterEach(() => { vi.clearAllMocks() vi.unstubAllGlobals() state.keys = [] + state.grants = [] state.webdavEnabled = true }) @@ -118,6 +157,7 @@ describe('Agent Access settings page', () => { await waitFor(() => expect(listAgentApiKeys).toHaveBeenCalledWith('org-1')) expect(await screen.findByText('settings.agentAccess.noKeys')).toBeTruthy() + expect(await screen.findByText('No delegated OAuth grants yet')).toBeTruthy() expect(screen.queryByLabelText('settings.agentAccess.nameLabel')).toBeNull() fireEvent.click(screen.getByRole('button', { name: 'settings.agentAccess.create' })) @@ -296,6 +336,124 @@ describe('Agent Access settings page', () => { expect(await screen.findByText('Owner or admin access is required')).toBeTruthy() expect(screen.getByRole('button', { name: 'settings.agentAccess.create' }).hasAttribute('disabled')).toBe(true) }) + + it('lists delegated OAuth grants and revokes them server-side', async () => { + state.grants = [ + { + id: 'grant-1', + clientId: 'zpan-agent', + clientName: 'ZPan Agent', + userId: 'user-1', + orgId: 'org-1', + workspaceName: 'Personal', + scopes: ['objects:read', 'shares:create'], + createdAt: '2026-07-29T12:00:00.000Z', + lastUsedAt: '2026-07-29T12:10:00.000Z', + status: 'active', + }, + ] + vi.mocked(revokeAgentOAuthGrant).mockResolvedValue(undefined) + + renderWithQuery() + + expect(await screen.findByText('Delegated OAuth Grants')).toBeTruthy() + expect(await screen.findByText('ZPan Agent')).toBeTruthy() + expect(screen.getByText('Files: read objects')).toBeTruthy() + expect(screen.getByText('Shares: create shares')).toBeTruthy() + + const revokeButtons = screen.getAllByRole('button', { name: 'settings.agentAccess.revoke' }) + fireEvent.click(revokeButtons[revokeButtons.length - 1]!) + const dialog = await screen.findByRole('dialog', { name: 'Revoke OAuth Grant' }) + fireEvent.click(within(dialog).getByRole('button', { name: 'settings.agentAccess.revoke' })) + + await waitFor(() => expect(revokeAgentOAuthGrant).toHaveBeenCalledWith('grant-1')) + expect(toast.success).toHaveBeenCalledWith('OAuth grant revoked') + }) + + it('renders OAuth consent from server context and submits full approval', async () => { + window.history.replaceState( + null, + '', + '/settings/agent-access?client_id=zpan-agent&redirect_uri=http%3A%2F%2F127.0.0.1%3A8484%2Fcallback&response_type=code&scope=openid%20offline_access%20objects%3Aread%20quota%3Aread', + ) + vi.mocked(getAgentOAuthConsentContext).mockResolvedValue({ + clientId: 'zpan-agent', + clientName: 'ZPan Agent', + instanceOrigin: 'https://zpan.example.test', + workspace: { id: 'org-1', name: 'Personal' }, + scopes: ['objects:read', 'quota:read'], + standardScopes: ['openid', 'offline_access'], + redirectUri: 'http://127.0.0.1:8484/callback', + grantLifetime: { accessTokenSeconds: 900, refreshTokenSeconds: 2_592_000 }, + }) + vi.mocked(submitAgentOAuthConsent).mockResolvedValue({ url: 'http://127.0.0.1:8484/callback?code=abc' }) + + renderWithQuery() + + expect(await screen.findByRole('heading', { name: 'Authorize ZPan Agent' })).toBeTruthy() + expect(screen.getByText('https://zpan.example.test')).toBeTruthy() + expect(screen.getByText('http://127.0.0.1:8484/callback')).toBeTruthy() + expect(screen.getByText('Files: read objects')).toBeTruthy() + expect(screen.getByText('Quota: read workspace quota')).toBeTruthy() + + fireEvent.click(screen.getByRole('button', { name: 'Approve Access' })) + + await waitFor(() => + expect(submitAgentOAuthConsent).toHaveBeenCalledWith({ + accept: true, + oauthQuery: window.location.search.slice(1), + }), + ) + expect(redirectExternal).toHaveBeenCalledWith('http://127.0.0.1:8484/callback?code=abc') + }) + + it('switches active workspace before OAuth consent and supports denial', async () => { + window.history.replaceState( + null, + '', + '/settings/agent-access?client_id=zpan-agent&redirect_uri=http%3A%2F%2F127.0.0.1%3A8484%2Fcallback&response_type=code&scope=objects%3Aread', + ) + vi.mocked(getAgentOAuthConsentContext).mockResolvedValue({ + clientId: 'zpan-agent', + clientName: 'ZPan Agent', + instanceOrigin: 'https://zpan.example.test', + workspace: { id: 'org-1', name: 'Personal' }, + scopes: ['objects:read'], + standardScopes: [], + redirectUri: 'http://127.0.0.1:8484/callback', + grantLifetime: { accessTokenSeconds: 900, refreshTokenSeconds: 2_592_000 }, + }) + vi.mocked(submitAgentOAuthConsent).mockResolvedValue({ url: 'http://127.0.0.1:8484/callback?error=access_denied' }) + + renderWithQuery() + await screen.findByRole('heading', { name: 'Authorize ZPan Agent' }) + + fireEvent.click(screen.getByRole('combobox')) + fireEvent.click(await screen.findByRole('option', { name: 'Team Alpha' })) + await waitFor(() => expect(setActive).toHaveBeenCalledWith({ organizationId: 'org-2' })) + + fireEvent.click(screen.getByRole('button', { name: 'Deny' })) + await waitFor(() => + expect(submitAgentOAuthConsent).toHaveBeenCalledWith({ + accept: false, + oauthQuery: window.location.search.slice(1), + }), + ) + expect(redirectExternal).toHaveBeenCalledWith('http://127.0.0.1:8484/callback?error=access_denied') + }) + + it('shows an expired OAuth request state when the consent context fails', async () => { + window.history.replaceState( + null, + '', + '/settings/agent-access?client_id=zpan-agent&redirect_uri=http%3A%2F%2F127.0.0.1%3A8484%2Fcallback', + ) + vi.mocked(getAgentOAuthConsentContext).mockRejectedValue(new Error('expired')) + + renderWithQuery() + + expect(await screen.findByRole('heading', { name: 'OAuth request expired' })).toBeTruthy() + }) }) describe('Settings layout tabs', () => { diff --git a/src/routes/_authenticated/settings/agent-access.tsx b/src/routes/_authenticated/settings/agent-access.tsx index 0d9b0744..d3e76d8e 100644 --- a/src/routes/_authenticated/settings/agent-access.tsx +++ b/src/routes/_authenticated/settings/agent-access.tsx @@ -1,7 +1,7 @@ import { type AgentGrantableScope, agentApiKeyShortcutOptions, agentScopeLabels } from '@shared/schemas' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' -import { Copy, KeyRound, Plus, RotateCw, Trash2 } from 'lucide-react' +import { Check, Copy, KeyRound, Plug, Plus, RotateCw, ShieldAlert, Trash2, X } from 'lucide-react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -21,8 +21,20 @@ import { Input } from '@/components/ui/input' 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 AgentApiKey, createAgentApiKey, listAgentApiKeys, revokeAgentApiKey, rotateAgentApiKey } from '@/lib/api' -import { useListOrganizations } from '@/lib/auth-client' +import { + type AgentApiKey, + type AgentOAuthGrant, + createAgentApiKey, + getAgentOAuthConsentContext, + listAgentApiKeys, + listAgentOAuthGrants, + revokeAgentApiKey, + revokeAgentOAuthGrant, + rotateAgentApiKey, + submitAgentOAuthConsent, +} 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: AgentAccessSettingsPage, @@ -54,6 +66,13 @@ function formatDate(value: string | null) { return value ? new Date(value).toLocaleString() : null } +function oauthQueryFromLocation(): string { + if (typeof window === 'undefined') return '' + const query = window.location.search.slice(1) + const params = new URLSearchParams(query) + return params.has('client_id') && params.has('redirect_uri') ? query : '' +} + function CopyButton({ value }: { value: string }) { const { t } = useTranslation() return ( @@ -267,11 +286,281 @@ function RevokeAgentKeyDialog({ apiKey, onClose }: { apiKey: AgentApiKey | null; ) } +function RevokeAgentOAuthGrantDialog({ grant, onClose }: { grant: AgentOAuthGrant | null; onClose: () => void }) { + const { t } = useTranslation() + const queryClient = useQueryClient() + const revokeMutation = useMutation({ + mutationFn: async () => { + if (!grant) return + await revokeAgentOAuthGrant(grant.id) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['agent-oauth-grants'] }) + toast.success(t('settings.agentAccess.oauthGrantRevokeSuccess')) + onClose() + }, + onError: (err) => toast.error(err.message), + }) + if (!grant) return null + return ( + !open && onClose()}> + + + {t('settings.agentAccess.oauthGrantRevokeTitle')} + + {t('settings.agentAccess.oauthGrantRevokeConfirm', { + client: grant.clientName, + workspace: grant.workspaceName ?? grant.orgId, + })} + + + + + + + + + ) +} + +function AgentOAuthConsentPanel({ oauthQuery, organizations }: { oauthQuery: string; organizations: Organization[] }) { + const { t } = useTranslation() + const queryClient = useQueryClient() + const [switchingOrgId, setSwitchingOrgId] = useState(null) + const [submitError, setSubmitError] = useState(null) + const consentQuery = useQuery({ + queryKey: ['agent-oauth-consent', oauthQuery], + queryFn: () => getAgentOAuthConsentContext(oauthQuery), + enabled: !!oauthQuery, + retry: false, + }) + const submitMutation = useMutation({ + mutationFn: (accept: boolean) => submitAgentOAuthConsent({ accept, oauthQuery }), + onSuccess: (result) => redirectExternal(result.url), + onError: (err) => setSubmitError(err instanceof Error ? err.message : t('settings.agentAccess.oauthConsentFailed')), + }) + + async function changeWorkspace(nextOrgId: string) { + setSwitchingOrgId(nextOrgId) + setSubmitError(null) + try { + const { error } = await setActive({ organizationId: nextOrgId }) + if (error) throw error + await queryClient.invalidateQueries({ queryKey: ['agent-oauth-consent', oauthQuery] }) + } catch (err) { + toast.error(err instanceof Error ? err.message : t('settings.agentAccess.oauthWorkspaceFailed')) + } finally { + setSwitchingOrgId(null) + } + } + + if (consentQuery.isLoading) { + return ( +
+

{t('common.loading')}

+
+ ) + } + + if (consentQuery.isError || !consentQuery.data) { + return ( +
+
+ +
+

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

+

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

+
+
+
+ ) + } + + const context = consentQuery.data + const lifetimeDays = Math.round(context.grantLifetime.refreshTokenSeconds / 86400) + + return ( +
+
+
+ + {t('settings.agentAccess.oauthConsentEyebrow')} +
+

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

+

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

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

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

+
+ {context.scopes.map((scope) => ( +
+ {t(agentScopeLabels[scope])} +
+ ))} +
+

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

+
+ + {submitError ? ( +

+ {submitError} +

+ ) : null} + +
+ + +
+
+ ) +} + +function AgentOAuthGrantsSection() { + const { t } = useTranslation() + const [revokingGrant, setRevokingGrant] = useState(null) + const grantsQuery = useQuery({ + queryKey: ['agent-oauth-grants'], + queryFn: listAgentOAuthGrants, + }) + const grants = grantsQuery.data?.items ?? [] + + return ( + + + {t('settings.agentAccess.oauthGrantsSection')} + {t('settings.agentAccess.oauthGrantsDescription')} + + + {grantsQuery.isLoading ? ( +

{t('common.loading')}

+ ) : grantsQuery.isError ? ( +

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

+ ) : grants.length === 0 ? ( +

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

+ ) : ( + + + + {t('settings.agentAccess.oauthClient')} + {t('settings.agentAccess.colWorkspace')} + {t('settings.agentAccess.colScopes')} + {t('settings.agentAccess.colCreated')} + {t('settings.agentAccess.colLastUsed')} + {t('settings.agentAccess.colStatus')} + {t('settings.agentAccess.colActions')} + + + + {grants.map((grant) => ( + + {grant.clientName} + {grant.workspaceName ?? grant.orgId} + +
+ {grant.scopes.map((scope) => ( + + {t(agentScopeLabels[scope])} + + ))} +
+
+ {formatDate(grant.createdAt)} + + {formatDate(grant.lastUsedAt) ?? t('settings.agentAccess.never')} + + + {t('settings.agentAccess.status.active')} + + + + +
+ ))} +
+
+ )} +
+ setRevokingGrant(null)} /> +
+ ) +} + export function AgentAccessSettingsPage() { const { t } = useTranslation() const queryClient = useQueryClient() const { data: organizationData } = useListOrganizations() const organizations = (organizationData ?? []) as Organization[] + const oauthQuery = oauthQueryFromLocation() const [orgId, setOrgId] = useState('') const [createOpen, setCreateOpen] = useState(false) const [revealedKey, setRevealedKey] = useState(null) @@ -300,8 +589,10 @@ export function AgentAccessSettingsPage() { } } + if (oauthQuery) return + return ( -
+
{t('settings.agentAccess.section')} @@ -414,6 +705,7 @@ export function AgentAccessSettingsPage() { setRevealedKey(null)} /> setRevoking(null)} /> +
) }