Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ Examples:
botInfo, _ := me["bot"].(map[string]interface{})
workspaceName, _ := botInfo["workspace_name"].(string)
name, _ := me["name"].(string)
integrationType := detectIntegrationType(botInfo)

render.Title("✓", "Authenticated")

Expand All @@ -147,6 +148,12 @@ Examples:

render.Field("Workspace", workspaceName)
render.Field("Bot", name)
if integrationType != "" {
render.Field("Integration", integrationType)
if integrationType == "internal" {
render.Field("Root page", "not allowed (share a parent page)")
}
}

// Show other available profiles
if len(profiles) > 1 {
Expand Down Expand Up @@ -371,8 +378,16 @@ Examples:
name, _ := me["name"].(string)
botInfo, _ := me["bot"].(map[string]interface{})
workspace, _ := botInfo["workspace_name"].(string)
integrationType := detectIntegrationType(botInfo)
fmt.Printf(" ✓ Auth: %s\n", name)
fmt.Printf(" ✓ Workspace: %s\n", workspace)
if integrationType != "" {
fmt.Printf(" ✓ Integration: %s\n", integrationType)
if integrationType == "internal" {
fmt.Println(" note: internal integrations cannot create pages at the workspace root;")
fmt.Println(" create a parent page in Notion and share it with this integration first.")
}
}

// Check 3: Can search
result, err := c.Search("", "", 1, "")
Expand All @@ -399,3 +414,28 @@ func init() {
authCmd.AddCommand(authDoctorCmd)
authCmd.AddCommand(authSwitchCmd)
}

// detectIntegrationType inspects the bot object returned by
// GET /v1/users/me and classifies the integration as "internal",
// "public", or "" when the shape is unrecognizable.
//
// Notion's response includes `bot.owner.type`, which is:
// - "workspace" for internal integrations (owned by the workspace)
// - "user" for public integrations installed by an individual user
//
// Ref: https://developers.notion.com/reference/user#bot
func detectIntegrationType(botInfo map[string]interface{}) string {
owner, ok := botInfo["owner"].(map[string]interface{})
if !ok {
return ""
}
ownerType, _ := owner["type"].(string)
switch ownerType {
case "workspace":
return "internal"
case "user":
return "public"
default:
return ""
}
}
53 changes: 53 additions & 0 deletions cmd/auth_integration_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package cmd

import "testing"

func TestDetectIntegrationType(t *testing.T) {
cases := []struct {
name string
bot map[string]interface{}
want string
}{
{
name: "internal (workspace-owned)",
bot: map[string]interface{}{
"owner": map[string]interface{}{"type": "workspace", "workspace": true},
},
want: "internal",
},
{
name: "public (user-owned)",
bot: map[string]interface{}{
"owner": map[string]interface{}{"type": "user"},
},
want: "public",
},
{
name: "missing owner",
bot: map[string]interface{}{},
want: "",
},
{
name: "unknown owner type",
bot: map[string]interface{}{
"owner": map[string]interface{}{"type": "planet"},
},
want: "",
},
{
name: "non-map owner",
bot: map[string]interface{}{
"owner": "oops",
},
want: "",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := detectIntegrationType(tc.bot)
if got != tc.want {
t.Errorf("got %q, want %q", got, tc.want)
}
})
}
}
4 changes: 4 additions & 0 deletions cmd/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ func setupAuthTest(t *testing.T) *httptest.Server {
"bot": map[string]interface{}{
"workspace_name": workspace,
"workspace_id": "ws-123",
"owner": map[string]interface{}{
"type": "workspace",
"workspace": true,
},
},
})
})
Expand Down
14 changes: 14 additions & 0 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,10 @@ func errorHint(code, message string) string {
case "rate_limited":
return "Too many requests. Wait a moment and try again"
case "validation_error":
if strings.Contains(message, "Internal integrations aren't owned") ||
strings.Contains(message, "insert_content") {
return internalIntegrationRootPageHint
}
if strings.Contains(message, "is not a property") {
return "Check property names with 'notion db view <id>' or 'notion page props <id>'"
}
Expand All @@ -421,3 +425,13 @@ func errorHint(code, message string) string {
}
return ""
}

// internalIntegrationRootPageHint is the one-paragraph explanation the CLI
// prints when an internal integration tries to create a workspace-root page.
// It's a multi-line string because that's the shape most users need: the
// API error is accurate but not directly actionable.
const internalIntegrationRootPageHint = "Internal integrations can't create pages at the workspace root.\n" +
" Workaround: create (or pick) a parent page in the Notion UI, share\n" +
" it with this integration, then pass its ID as the parent:\n" +
" notion page create <shared-page-id> --title \"...\"\n" +
" To list pages shared with your integration: notion page list"
2 changes: 2 additions & 0 deletions internal/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ func TestErrorHint(t *testing.T) {
{"rate_limited", "Rate limited", "Wait"},
{"validation_error", "is not a property that exists", "notion db view"},
{"validation_error", "body failed validation", "--debug"},
{"validation_error", "Internal integrations aren't owned by a single user, so creating workspace-level private pages is not supported.", "share"},
{"validation_error", "use a public integration with insert_content capability", "share"},
{"conflict_error", "conflict", "Retry"},
{"internal_server_error", "error", "Notion's servers"},
{"service_unavailable", "unavailable", "Try again"},
Expand Down
Loading